BTF-resolved functions are a mechanism for Windows kernel drivers to expose functions that can be called directly
from eBPF programs, without requiring those drivers to be program-type specific information providers. Unlike helper
functions, which are identified by a fixed numeric ID, BTF-resolved functions are resolved by name via BTF (BPF Type Format) and
called using the call_btf instruction with a BTF ID. Module disambiguation is handled by caller-side mapping of
(module GUID, function name) to that BTF ID.
This document uses the term "BTF-resolved functions". Linux documentation commonly calls the same concept "kfuncs".
Design status: This document describes a proposed design. The BTF-resolved-function NPI IDs, store APIs, and C types shown here are planned interfaces and are not yet present in current public headers.
| Aspect | Helper Functions by Static ID | BTF-resolved functions |
|---|---|---|
| Resolution | Fixed numeric ID (0-65535 for global, >65535 for program-type specific) | Session-local BTF ID resolved from (module GUID, function name) |
| Provider | eBPF runtime (global) or program-type extension (type-specific) | Any driver registered as a BTF-resolved function provider (independent of program-type providers) |
| Instruction | call imm (src=0) |
call_btf (src=2, imm=btf_id, offset=0) |
| Namespace | Single global namespace with reserved ranges | Per-module namespace, disambiguated by module GUID |
| Discovery | Compile-time via header inclusion | Compile-time via header with BTF metadata |
BTF-resolved functions enable scenarios such as:
- Exposing driver-specific functionality to eBPF programs without modifying the eBPF core
- Providing specialized operations that are too complex or domain-specific to be general helpers
- Allowing drivers that are not program-type specific providers to extend eBPF capabilities in a modular fashion
┌─────────────────────────────────────────────────────────────────────────────┐
│ Compile Time │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ my_prog.c │ │ clang/LLVM │ │ my_prog.o (ELF) │ │
│ │ #include ├───>│ -g (BTF) ├───>│ - .ksyms section in BTF │ │
│ │ <helpers.h> │ │ │ │ - extern calls with decl_tag │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────────────────┐
│ Verification Time │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ bpf2c / netsh │ │
│ │ 1. Parse .ksyms from BTF │ │
│ │ 2. Resolve each BTF-resolved function name: │ │
│ │ - Query registry (BtfResolvedFunctions) for prototype + GUID │ │
│ │ - Assign session-local BTF ID for (module GUID, function name) │ │
│ │ - Provide resolved call contract to verifier │ │
│ │ 3. Verifier rewrites extern calls to call_btf(btf_id) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────────────────┐
│ Load Time │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Native Module (.sys) │ │
│ │ 1. Register as NMR client for BTF-resolved function NPI │ │
│ │ 2. For each provider (by module GUID): │ │
│ │ - Attach to provider, receive function addresses │ │
│ │ 3. Populate import table with resolved addresses │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────────────────┐
│ Runtime │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Program Execution │ │
│ │ - call_btf resolves to indirect call through import table │ │
│ │ - If provider detaches, addresses become NULL │ │
│ │ - Program invocation fails until provider reattaches │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Drivers that expose BTF-resolved functions must author a header file that eBPF program developers include. This header contains:
- Function declarations with appropriate attributes
- BTF metadata to place the function in the
.ksymssection - A declaration tag identifying the provider module
// Copyright (c) Example Corp
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
// Module GUID for this BTF-resolved function provider
// Format: "module_id:{GUID}"
#define MY_DRIVER_MODULE "module_id:{12345678-1234-1234-1234-123456789abc}"
// Declare a BTF-resolved function with BTF metadata
// __attribute__((section(".ksyms"))) places the symbol in the .ksyms BTF section
// __attribute__((btf_decl_tag(MY_DRIVER_MODULE))) associates it with the module
#define DECLARE_BTF_RESOLVED_FUNCTION(ret, name, ...) \
extern ret name(__VA_ARGS__) \
__attribute__((section(".ksyms"))) \
__attribute__((btf_decl_tag(MY_DRIVER_MODULE)))
// Example BTF-resolved function declarations
DECLARE_BTF_RESOLVED_FUNCTION(int, my_driver_lookup, uint64_t key, void* value, uint32_t value_size);
DECLARE_BTF_RESOLVED_FUNCTION(int, my_driver_update, uint64_t key, const void* value, uint32_t value_size);
DECLARE_BTF_RESOLVED_FUNCTION(void, my_driver_log, const char* message, uint32_t length);- The module GUID must be unique to your driver
- Use the same GUID for registry publication and NMR registration
- The GUID is embedded in the compiled eBPF program and used at load time to find your driver
BTF-resolved function signatures must follow the same constraints as helper functions:
- Maximum of 5 arguments
- Arguments must be types representable in
ebpf_argument_type_t - Return type must be representable in
ebpf_return_type_t - Function must be safe to call at the IRQL where eBPF programs execute
Drivers that expose BTF-resolved functions must publish their function metadata to the Windows registry. This allows the verifier to discover and validate BTF-resolved function calls at verification time. The eBPF store can be rooted at either HKCU or HKLM, so the path below is shown relative to the store root.
Software\eBPF\Providers\BtfResolvedFunctions
└── {12345678-1234-1234-1234-123456789abc} <- Module GUID
├── Version: REG_DWORD = 1
├── Size: REG_DWORD = <size of provider data>
└── Functions
├── my_driver_lookup
│ ├── Prototype: REG_SZ = "int my_driver_lookup(uint64_t, void*, uint32_t)"
│ ├── ReturnType: REG_DWORD = <ebpf_return_type_t value>
│ ├── Arguments: REG_BINARY = <array of ebpf_argument_type_t>
│ └── Flags: REG_DWORD = <flags>
├── my_driver_update
│ └── ...
└── my_driver_log
└── ...
Similar to program type registration, BTF-resolved function providers can publish metadata via a store API like the following:
// Structure describing a BTF-resolved function prototype
typedef struct _ebpf_btf_resolved_function_prototype
{
ebpf_extension_header_t header;
const char* name;
ebpf_return_type_t return_type;
ebpf_argument_type_t arguments[5];
uint32_t flags;
} ebpf_btf_resolved_function_prototype_t;
// Structure describing a BTF-resolved function provider
typedef struct _ebpf_btf_resolved_function_provider_info
{
ebpf_extension_header_t header;
GUID module_guid;
uint32_t btf_resolved_function_count;
const ebpf_btf_resolved_function_prototype_t* btf_resolved_function_prototypes;
} ebpf_btf_resolved_function_provider_info_t;
// Proposed API to register BTF-resolved function provider information
ebpf_result_t
ebpf_store_update_btf_resolved_function_provider_information(
_In_ const ebpf_btf_resolved_function_provider_info_t* provider_info);This API is not currently present in include/ebpf_store_helper.h.
The PREVAIL verifier supports BTF-resolved functions through caller-side preprocessing plus a platform callback during verification.
During ELF parsing, bpf2c or netsh (outside PREVAIL platform callbacks) identifies external function calls from the
.ksyms BTF section and assigns session-local BTF IDs.
The caller (bpf2c or netsh) does this in two steps:
- Build function and module mappings from BTF
- Enumerate function symbols from the
.ksymssection - Enumerate top-level
BTF_KIND_DECL_TAGentries (entries with no parent) - Parse each decl tag string (for example,
module_id:{guid}) and use the tag's function reference to build the module-to-function mapping for.ksymsfunctions
- Enumerate function symbols from the
- Resolve each symbol into a session-local BTF ID
- Look up each function in the registry under
BtfResolvedFunctions\{module_guid}\Functions\{name} - Allocate a deterministic session-local BTF ID for each
(module_guid, function_name)pair - Build both mappings:
(module_guid, function_name) -> btf_idandbtf_id -> (module_guid, function_name)
- Look up each function in the registry under
During verification, when PREVAIL encounters a call_btf instruction, it invokes
ebpf_platform_t::resolve_kfunc_call:
// external/ebpf-verifier/src/platform.hpp
typedef std::optional<Call> (*ebpf_resolve_kfunc_call_fn)(
int32_t btf_id,
const ProgramInfo* info,
std::string* why_not);The caller implementation of resolve_kfunc_call maps btf_id -> (module_guid, function_name), loads the function
prototype from the registry, and returns the corresponding PREVAIL Call contract.
The verifier rewrites external calls to BTF-resolved functions as follows:
| Before (extern call) | After (call_btf) |
|---|---|
call imm=0 (src=1) |
call imm=btf_id offset=0 (src=2) |
In current PREVAIL, CallBtf carries only btf_id (the imm field), and offset must be 0.
BTF IDs are assigned by Clang/LLVM when the eBPF object is compiled and are not stable across builds. In eBPF for
Windows, the verifier-rewritten call_btf form is ephemeral and consumed by the verifier/bpf2c pipeline; it is not a
long-term persisted interface. Unlike Linux, eBPF for Windows does not expose kernel-code BTF IDs as a public API.
When generating native code, bpf2c emits a BTF-resolved function import table alongside the existing helper import table.
typedef struct _btf_resolved_function_entry
{
uint64_t zero_marker; // Marker for section parsing (must precede header per bpf2c convention)
ebpf_native_module_header_t header;
const char* name; // Function name
GUID module_guid; // Module GUID for NMR binding
} btf_resolved_function_entry_t;The program_runtime_context_t structure is extended to include BTF-resolved function addresses:
typedef struct _btf_resolved_function_data
{
ebpf_native_module_header_t header; // Header for versioning and compatibility
helper_function_t address; // Resolved function address
} btf_resolved_function_data_t;
// Proposed extension to include/bpf2c.h program_runtime_context_t.
typedef struct _program_runtime_context
{
ebpf_native_module_header_t header;
helper_function_data_t* helper_data;
map_data_t* map_data;
global_variable_section_data_t* global_variable_section_data;
btf_resolved_function_data_t* btf_resolved_function_data; // NEW: BTF-resolved function addresses
} program_runtime_context_t;For a BTF-resolved function call, bpf2c generates:
// BTF-resolved function import table
static btf_resolved_function_entry_t _btf_resolved_functions[] = {
{0, BTF_RESOLVED_FUNCTION_ENTRY_HEADER, "my_driver_lookup",
{0x12345678, 0x1234, 0x1234, {0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}}},
{0, BTF_RESOLVED_FUNCTION_ENTRY_HEADER, "my_driver_update",
{0x12345678, 0x1234, 0x1234, {0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}}},
};
// In program code, BTF-resolved function calls go through the runtime context
result = ((int (*)(uint64_t, void*, uint32_t))runtime_context->btf_resolved_function_data[0].address)(key, value, size);The program info hash (used for proof of verification) must include BTF-resolved function dependencies:
- Count of BTF-resolved functions used (to prevent length-extension attacks)
- For each BTF-resolved function used (in deterministic order by module GUID, then function name):
btf_resolved_function_entry_t::namebtf_resolved_function_entry_t::module_guidebpf_btf_resolved_function_prototype_t::return_type- Each element of
ebpf_btf_resolved_function_prototype_t::arguments ebpf_btf_resolved_function_prototype_t::flags(only if non-default)
Drivers that expose BTF-resolved functions register as NMR providers for the BTF-resolved function NPI.
The GUID name below is a proposed identifier and is not currently present in include/ebpf_extension_uuids.h.
// BTF-resolved function NPI ID
static const GUID EBPF_BTF_RESOLVED_FUNCTION_EXTENSION_IID = {
0xaabbccdd, 0x1234, 0x5678, {0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}};When registering as an NMR provider:
NpiId: Set to the proposedEBPF_BTF_RESOLVED_FUNCTION_EXTENSION_IIDonce this identifier is addedModuleId: Set to the driver's module GUID (same as in the header and registry)NpiSpecificCharacteristics: Pointer toebpf_btf_resolved_function_provider_data_t
typedef struct _ebpf_btf_resolved_function_provider_data
{
ebpf_extension_header_t header;
uint32_t btf_resolved_function_count;
const ebpf_btf_resolved_function_prototype_t* btf_resolved_function_prototypes;
const uint64_t* btf_resolved_function_addresses; // Addresses of the BTF-resolved function implementations
} ebpf_btf_resolved_function_provider_data_t;The BTF-resolved function provider does not require a dispatch table. Function addresses are provided directly in the provider data.
When a native module (.sys) is loaded, it attaches to BTF-resolved function providers to resolve function addresses.
The native module skeleton registers as an NMR client for the BTF-resolved function NPI with a wildcard module ID, meaning it will receive attach callbacks for all registered BTF-resolved function providers.
When NMR calls the client attach callback for a BTF-resolved function provider:
- Check if the provider's module GUID matches any entry in the BTF-resolved function import table
- If yes:
- Store the binding handle
- Copy function addresses to the
btf_resolved_function_dataarray in the runtime context - Record the binding in the module's BTF-resolved function provider binding list
- If no: Return
STATUS_NOINTERFACEto decline the binding
When a BTF-resolved function provider detaches:
- Set the corresponding
btf_resolved_function_dataaddresses to NULL - Mark the binding as detached
- If the program is currently executing, wait for completion
- Invoke
btf_resolved_function_addresses_changed_callbackif registered
A single eBPF program may use BTF-resolved functions from multiple providers. The native module maintains:
typedef struct _ebpf_btf_resolved_function_provider_binding
{
GUID module_guid;
HANDLE nmr_binding_handle;
bool attached;
} ebpf_btf_resolved_function_provider_binding_t;Note: This is distinct from
ebpf_btf_resolved_function_binding_tin Section 10, which tracks per-function bindings and includes an additionalprovider_datafield. Provider bindings here track per-provider NMR attachment state, while function bindings in Section 10 track resolution of individual BTF-resolved functions.
All required providers must be attached before the program can execute.
Before invoking an eBPF program that uses BTF-resolved functions:
- Check that all required BTF-resolved function providers are attached
- If any provider is detached, return
EBPF_EXTENSION_FAILED_TO_LOAD - Take rundown protection on all BTF-resolved function bindings
- Execute the program (BTF-resolved function calls go through runtime context indirection)
- Release rundown protection
Similar to helper functions, BTF-resolved function address changes are propagated via callback:
typedef ebpf_result_t (*ebpf_btf_resolved_function_addresses_changed_callback_t)(
size_t address_count,
_In_reads_(address_count) const uint64_t* addresses,
_Inout_ void* context);For JIT-compiled programs, this callback updates the jump table. For native programs, the runtime context is updated directly.
| Scenario | Behavior |
|---|---|
| BTF-resolved function provider not registered | Program load fails with EBPF_EXTENSION_FAILED_TO_LOAD |
| BTF-resolved function provider detaches while program loaded | Program invocation fails until provider reattaches |
| BTF-resolved function provider detaches during program execution | Execution completes, subsequent invocations fail |
The ebpf_program_t structure requires the following additions to support BTF-resolved functions:
// New structure for tracking BTF-resolved function provider bindings
typedef struct _ebpf_btf_resolved_function_binding
{
GUID module_guid;
HANDLE nmr_binding_handle;
const ebpf_btf_resolved_function_provider_data_t* provider_data;
bool attached;
} ebpf_btf_resolved_function_binding_t;
// Additions to ebpf_program_t
typedef struct _ebpf_program_t
{
// ... existing fields ...
// BTF-resolved function support
_Guarded_by_(lock) ebpf_btf_resolved_function_binding_t* btf_resolved_function_bindings;
_Guarded_by_(lock) size_t btf_resolved_function_binding_count;
_Guarded_by_(lock) uint64_t* btf_resolved_function_addresses; // Resolved addresses array
_Guarded_by_(lock) size_t btf_resolved_function_count;
_Guarded_by_(lock) ebpf_btf_resolved_function_addresses_changed_callback_t btf_resolved_function_addresses_changed_callback;
_Guarded_by_(lock) void* btf_resolved_function_addresses_changed_context;
} ebpf_program_t;- Program Creation: Allocate arrays based on BTF-resolved function import table size
- NMR Client Registration: Register for BTF-resolved function NPI to receive provider attach callbacks
- Provider Attach: Populate
btf_resolved_function_bindingsandbtf_resolved_function_addresses - Program Load: Verify all required providers are attached
- Provider Detach: Clear addresses, invoke callback, wait for rundown
- Program Free: Deregister NMR client, free arrays
BTF-resolved function providers are kernel drivers and must be signed according to Windows driver signing requirements. The eBPF runtime does not perform additional authentication beyond what Windows enforces.
The verifier validates that:
- All BTF-resolved function calls have valid prototypes registered
- Argument types match the declared prototype
- Return values are handled correctly
The program info hash includes BTF-resolved function dependencies, ensuring that a signed native module cannot call BTF-resolved functions that were not present during verification.
Each BTF-resolved function provider is responsible for:
- Validating arguments passed from eBPF programs
- Ensuring safe execution at the IRQL where programs run
- Not exposing security-sensitive operations without proper authorization
A future enhancement could support "BTF-resolved function sets" - groups of related functions that are versioned together. This would simplify compatibility management when BTF-resolved function signatures evolve.
A policy mechanism could allow administrators to control which programs can call which BTF-resolved functions, providing finer-grained security control.
Currently, BTF-resolved function metadata must be in the registry at verification time. Dynamic discovery could allow programs to query available BTF-resolved functions at runtime, enabling more flexible extension models.