---
sidebar_position: 5.6
title: BTF types (yeet:btf)
---

# yeet:btf

`yeet:btf` queries the kernel's [BTF](https://docs.kernel.org/bpf/btf.html) — the type information the running kernel carries about its own structs, unions, enums, and functions. Use it to look up a type's full layout, resolve a member's byte offset, or produce the exact deref-and-read *plan* to reach a deeply nested field. It is the type oracle a script (or an AI generating one) consults so it never hand-codes struct offsets or pointer arithmetic that break on the next kernel.

```js
import btf from 'yeet:btf';

const t = await btf.type('task_struct');
console.log(t.kind, t.size, t.members.length);          // struct 9472 251

const { offset, type } = await btf.offsetOf('task_struct', 'pid');
console.log(offset, type.name);                          // 2464 int

// The hops a bounded interpreter runs to read sock->sk_socket->file->f_inode->i_ino:
const plan = await btf.walk('sock', 'sk_socket.file.f_inode.i_ino');
console.log(plan.hops);
// [ { op: 'deref', at: 640, width: 8 },
//   { op: 'deref', at: 40,  width: 8 },
//   { op: 'deref', at: 0,   width: 8 },
//   { op: 'load',  at: 64,  width: 8 } ]
```

Every op is asynchronous and answered daemon-side, off the V8 thread, against a **single process-wide index** parsed on first use from the running kernel (or a sidecar — see [BTF source](#btf-source)). The parse is shared by every isolate and unloaded when it goes idle, so a script that never touches BTF pays nothing.

:::note
The exact offsets and sizes above are illustrative — they are kernel- and config-specific. That is the whole point: query them at runtime rather than baking them into a script.
:::

## Importing

```js
import btf, { type, expand, offsetOf, walk } from 'yeet:btf';
```

| Export | Kind | Description |
|---|---|---|
| [`type`](#type) | function | The full description of a named type — layout, members, values |
| [`expand`](#expand) | function | The full description behind a nested reference — by id, or by name |
| [`offsetOf`](#offsetof) | function | A member's pointer-free byte offset within a type |
| [`walk`](#walk) | function | The deref-and-read plan to reach a member across pointers |

The default export bundles all four (`btf.type`, `btf.expand`, `btf.offsetOf`, `btf.walk`).

## Concepts

### BTF source {#btf-source}

The index is built from one of two sources, chosen by the daemon at startup:

| Source | When | Loaded from |
|---|---|---|
| Running kernel (default) | no override | the kernel's own `vmlinux` BTF |
| Custom sidecar | `yeetd --btf-custom-path <file>` | a `.btf` file or a vmlinux image with a `.BTF` section |

Use `--btf-custom-path` to query a kernel other than the host's — a target you are building a probe for, a stripped kernel with BTF shipped separately, or a captured `vmlinux` for reproducible offsets in a test. The same path also seeds libbpf's CO-RE relocations for `.bpf.o` loads, so a probe and its `yeet:btf` queries agree on one type universe.

### Type names and kinds {#kinds}

A query names a type by its BTF name (`task_struct`, `sk_buff`, `pid_t`). A name can be shared across **kinds** — a `struct foo` and a `typedef foo`, say — so each query accepts an optional `kind` to disambiguate. Omit it when the name is unique; supply it when a bare name is [`AMBIGUOUS_TYPE`](#errors).

```js
await btf.type('pid_t');                    // unique → the typedef
await btf.type('sigval', { kind: 'union' }); // pin the union over the fwd/typedef
```

Every type carries a `kind` tag, one of:

`void` · `int` · `float` · `ptr` · `array` · `struct` · `union` · `enum` · `fwd` · `typedef` · `const` · `volatile` · `restrict` · `typetag` · `func` · `funcproto` · `var` · `datasec` · `other`

The `kind` decides which fields a [`TypeDesc`](#typedesc) carries: `members` for a struct/union, `values` for an enum, `elem`/`nelems` for an array, `params`/`ret` for a function prototype, `target` for anything that points at or wraps one other type (a pointer, typedef, or qualifier).

### One-level references {#type-refs}

To keep a result from dragging a type's entire transitive definition across the wire, a nested type appears as a lightweight [`TypeRef`](#typeref) — just `{ id, kind, name, size }`, not the full body. Pass the ref's `id` to [`expand`](#expand) to open it:

```js
const t = await btf.type('task_struct');
const seRef = t.members.find(m => m.name === 'se').type; // TypeRef, kind "struct"
const se = await btf.expand(seRef.id);                   // expand it → full sched_entity
```

This is how a walk over a struct's whole embedded closure descends: kernel structs are full of **anonymous** nested unions and structs (`name` absent — `sock_common` alone holds eight anonymous unions), and with no name to re-query, the `id` is the only handle a branch has. `expand` takes named refs too — by id, or by the name itself as a convenience — but the id path is the one that always works, and being unambiguous it never needs a [`kind`](#kinds) hint.

### The access plan {#access-plan}

`walk` is the reason this module exists. Given a dotted member path, it returns the ordered [`hops`](#hop) a **bounded interpreter** — a small, verifiable read machine like the one in a walk-VM probe — executes to arrive at the field:

- **`deref`** — read a pointer of `width` bytes at offset `at`, follow it, and open a fresh offset window at the target.
- **`load`** — the terminal scalar read: `width` bytes at offset `at` in the current window.
- **`field`** — a terminal that lands on an aggregate (a struct/array with no single scalar to read); only its `at` offset is given.

Because the daemon composes the pointer arithmetic, the interpreter never does — it just runs the hops. That is what lets an AI discover a kernel's layout and emit a correct, bounded probe DSL without embedding brittle offsets.

```js
const plan = await btf.walk('task_struct', 'mm.owner.comm');
for (const h of plan.hops) {
  // feed each hop to your read machine
}
console.log(plan.type.name);   // resolved terminal type
```

### `offsetOf` vs `walk` {#offsetof-vs-walk}

`offsetOf` is the shortcut for the common case: a member reachable **without crossing a pointer**, so a single byte offset answers it. It rejects [`POINTER_IN_PATH`](#errors) the moment a path would dereference — reach for `walk` there.

```js
await btf.offsetOf('task_struct', 'se.sum_exec_runtime'); // { offset, type } — all in one object
await btf.offsetOf('task_struct', 'mm.owner');            // rejects POINTER_IN_PATH (mm is a pointer)
await btf.walk('task_struct', 'mm.owner');                // the deref chain instead
```

A `walk` whose path never crosses a pointer collapses to a single `load` or `field` hop — the same offset `offsetOf` would return.

### Bitfields {#bitfields}

A member declared as a C bitfield (`unsigned flag : 1`) can't be addressed by byte offset alone. Both [`walk`](#walk) and a struct member's [`MemberDesc`](#memberdesc) carry a [`BitField`](#bitfield) with `bit_offset` and `bit_size`. In a `walk` plan the terminal `load` reads the member's **aligned storage unit** and `bit_offset` is relative to that unit, so a high bit position never pushes the read past the type — mask and shift the loaded word to extract the value.

```js
const plan = await btf.walk('task_struct', 'flags'); // depends on the kernel's task_struct
if (plan.bitfield) {
  const { bit_offset, bit_size } = plan.bitfield;
  const mask = ((1n << BigInt(bit_size)) - 1n) << BigInt(bit_offset);
}
```

### Caching and lifetime {#caching}

The first query parses the whole kernel BTF into a compact, shared index (~5 MiB for a full `vmlinux`) and every later query is answered from it. After a spell with no queries the index is unloaded and its memory returned; the next query rebuilds it transparently. Nothing to open or close — the module is stateless from a script's point of view.

---

## <Fn /> type {#type}

```ts
type(name: string, opts?: { kind?: string }): Promise<TypeDesc>
```

The full [`TypeDesc`](#typedesc) of the named type: its kind, size, and the kind-specific body (members, enum values, array element, function signature, or referent). Pass [`kind`](#kinds) to disambiguate a name shared across kinds. Rejects [`UNKNOWN_TYPE`](#errors) (with near-miss suggestions) when no type matches, or [`AMBIGUOUS_TYPE`](#errors) when several do and no `kind` was given.

```js
const s = await btf.type('sk_buff');
console.log(s.size, s.members.map(m => m.name));

const e = await btf.type('bpf_prog_type');
console.log(e.values.slice(0, 3));   // [{ name: 'BPF_PROG_TYPE_UNSPEC', value: 0 }, …]
```

## <Fn /> expand {#expand}

```ts
expand(target: number | string): Promise<TypeDesc>
```

The full [`TypeDesc`](#typedesc) behind a nested [`TypeRef`](#typeref) — the [re-expansion step](#type-refs) for walking beyond one level. A **number** is a ref's `id`, the only way into an anonymous struct or union (which has no name for [`type`](#type) to resolve); ids come from results (`member.type.id`, `desc.target.id`, `desc.elem.id`) and are stable for the lifetime of the loaded index, so memoize by id when walking a closure. A **string** is accepted as a convenience for named refs — it resolves like [`type`](#type) but takes no `kind` hint, so a name shared across kinds rejects [`AMBIGUOUS_TYPE`](#errors); reach for `type` there. An id outside the index rejects [`UNKNOWN_ID`](#errors).

```js
const sc = await btf.type('sock_common');
const anon = sc.members.filter(m => !m.name);  // inline unions — no name to re-query
const u = await btf.expand(anon[0].type.id);
console.log(u.members.map(m => `${m.name}@${m.offset}`)); // ['skc_addrpair@0', …]
```

## <Fn /> offsetOf {#offsetof}

```ts
offsetOf(type: string, path: string): Promise<FieldOffset>
```

The pointer-free byte `offset` of the dotted member `path` within `type`, plus a [`TypeRef`](#typeref) to the member's resolved type, as a [`FieldOffset`](#fieldoffset). Anonymous unions and structs along the path are transparent — name only the members you care about. Rejects [`POINTER_IN_PATH`](#errors) if the path crosses a pointer (use [`walk`](#walk)), [`NO_MEMBER`](#errors) (listing the siblings) for an unknown segment, or [`NOT_COMPOSITE`](#errors) when a segment isn't a struct/union.

```js
const { offset, type } = await btf.offsetOf('task_struct', 'se.sum_exec_runtime');
// offset is the byte displacement; type is a TypeRef to u64 (or similar)
```

## <Fn /> walk {#walk}

```ts
walk(type: string, path: string): Promise<LoadPlan>
```

The full access plan to reach `path` from `type`: the ordered [`hops`](#hop) a bounded interpreter runs, the resolved terminal [`type`](#typeref), and a [`bitfield`](#bitfield) position when the target is a bitfield. Each crossed pointer becomes a `deref` hop that opens a fresh offset window; the walk ends in a `load` (scalar) or `field` (aggregate) hop. See [The access plan](#access-plan). Rejects with the same path errors as [`offsetOf`](#offsetof) except `POINTER_IN_PATH` — `walk` is the pointer-crossing form.

```js
const plan = await btf.walk('sock', 'sk_socket.file.f_inode.i_ino');
// plan.hops → [deref, deref, deref, load]; plan.type → the i_ino type
```

---

## Data types

### <Interface /> TypeDesc {#typedesc}

The full description of one type, returned by [`type`](#type) and [`expand`](#expand). Fields irrelevant to the kind are omitted.

```ts
interface TypeDesc {
  id: number;              // this type's BTF id
  kind: string;            // see Type names and kinds
  name?: string;           // absent for an anonymous type
  size?: number;           // byte size, when the kind has one
  encoding?: IntEncoding;  // int — how the integer reads
  members?: MemberDesc[];  // struct / union
  values?: EnumValue[];    // enum
  target?: TypeRef;        // ptr / typedef / const / volatile / restrict / typetag / func → its referent
  params?: ParamDesc[];    // funcproto
  ret?: TypeRef;           // funcproto return type
  elem?: TypeRef;          // array element type
  nelems?: number;         // array element count
}
```

### <Interface /> TypeRef {#typeref}

A lightweight one-level reference to a type — enough to name it in a member list or a result without its full body. [Re-query by name](#type-refs) to expand it.

```ts
interface TypeRef {
  id: number;      // the referenced type's BTF id
  kind: string;
  name?: string;   // absent for an anonymous type
  size?: number;
}
```

### <Interface /> IntEncoding {#intencoding}

How an `int` type reads, carried on its [`TypeDesc`](#typedesc) as independent flags — test a field rather than matching a string. An unsigned integer has all three false.

```ts
interface IntEncoding {
  signed: boolean;
  bool: boolean;   // C _Bool
  char: boolean;
}
```

### <Interface /> MemberDesc {#memberdesc}

One member of a struct or union in a [`TypeDesc`](#typedesc).

```ts
interface MemberDesc {
  name?: string;       // absent for an anonymous member
  offset: number;      // byte offset within the aggregate
  bitfield?: BitField; // present only for bitfield members
  type: TypeRef;       // the member's type
}
```

### <Interface /> ParamDesc {#paramdesc}

One parameter of a function prototype (`kind: "funcproto"`).

```ts
interface ParamDesc {
  name?: string;  // absent for an unnamed parameter
  type: TypeRef;
}
```

### <Interface /> EnumValue {#enumvalue}

One enumerator of an enum (`kind: "enum"`).

```ts
interface EnumValue {
  name: string;
  value: number;
}
```

### <Interface /> FieldOffset {#fieldoffset}

Returned by [`offsetOf`](#offsetof).

```ts
interface FieldOffset {
  offset: number;  // pointer-free byte offset of the member
  type: TypeRef;   // the member's resolved type
}
```

### <Interface /> LoadPlan {#loadplan}

Returned by [`walk`](#walk).

```ts
interface LoadPlan {
  hops: Hop[];         // run in order to reach the field
  type: TypeRef;       // the resolved terminal type
  bitfield?: BitField; // present only when the target is a bitfield
}
```

### <Interface /> Hop {#hop}

One step of an access plan, tagged by `op`. See [The access plan](#access-plan).

```ts
type Hop =
  | { op: 'deref'; at: number; width: number } // read a pointer at `at`, follow it, open a fresh window
  | { op: 'load';  at: number; width: number } // terminal scalar read of `width` bytes at `at`
  | { op: 'field'; at: number };               // terminal aggregate — offset only, nothing to read
```

`at` is always relative to the **current** window: a fresh window opens after each `deref`, so offsets restart from 0 following one.

### <Interface /> BitField {#bitfield}

A bitfield's position within its loaded storage unit. See [Bitfields](#bitfields).

```ts
interface BitField {
  bit_offset: number; // bits from the start of the loaded unit
  bit_size: number;   // width in bits
}
```

---

## Errors {#errors}

Every function rejects with a plain object — **not** an `Error` — of the shape `{ code, message }`. Match on `code`, not on the message string (the message is human-facing and carries repair hints — near-miss type names, the available members at a failing path segment):

```js
try {
  await btf.offsetOf('task_struct', 'mm.owner');
} catch (e) {
  if (e.code === 'POINTER_IN_PATH') {
    const plan = await btf.walk('task_struct', 'mm.owner'); // recover with the deref chain
  }
}
```

| `code` | Raised when |
|---|---|
| `INVALID_ARGS` | An argument is missing or the wrong type (a non-string `name`, `type`, or `path`; an `expand` target that is neither an id nor a name) |
| `UNKNOWN_TYPE` | No type matches the name (message suggests near-miss names) |
| `UNKNOWN_ID` | `expand` was given an id outside the loaded index |
| `AMBIGUOUS_TYPE` | The name is shared across kinds and no `kind` was given |
| `NO_MEMBER` | A path segment names no member of its container (message lists the siblings) |
| `NOT_COMPOSITE` | A path segment tries to descend into a non-struct/union |
| `POINTER_IN_PATH` | `offsetOf` was given a path that crosses a pointer — use `walk` |
| `OFFSET_OVERFLOW` | A member's bit offset exceeds the index's 24-bit packing limit (a malformed or pathological type) |
| `BTF_LOAD_FAILED` | The kernel BTF (or the `--btf-custom-path` sidecar) could not be loaded or parsed |
| `SERIALIZE_FAILED` | A result could not be serialized back to the isolate |

---

## Recipes

### Build a deref plan for a bounded interpreter

```js
import btf from 'yeet:btf';

// Turn a human-readable field path into hops a read machine can run.
const plan = await btf.walk('task_struct', 'mm.owner.comm');
const program = plan.hops.map(h =>
  h.op === 'deref' ? ['DEREF', h.at, h.width]
  : h.op === 'load' ? ['LOAD', h.at, h.width]
  : ['FIELD', h.at]
);
// hand `program` to your walk-VM probe
```

### Resolve a member offset without crossing a pointer

```js
const { offset } = await btf.offsetOf('sk_buff', 'len');
// `offset` is the byte displacement to read `len` directly off an sk_buff pointer
```

### Enumerate a struct's members

```js
const t = await btf.type('inode');
for (const m of t.members) {
  console.log(`${m.offset}\t${m.name}\t${m.type.name ?? '(anon ' + m.type.kind + ')'}`);
}
```

### Classify every scalar in a struct's embedded closure

```js
// One expand per distinct id, memoized — anonymous unions flatten into
// their parent at an accumulated offset, and encoding flags decide the
// read: signed, boolean, or plain unsigned.
const seen = new Map();
const open = (id) => seen.get(id) ?? seen.set(id, btf.expand(id)).get(id);

const QUALIFIERS = new Set(['typedef', 'const', 'volatile', 'restrict', 'typetag']);
async function peel(id) {
  let d = await open(id);
  while (QUALIFIERS.has(d.kind) && d.target) d = await open(d.target.id);
  return d;
}

async function scalars(desc, base = 0, out = []) {
  for (const m of desc.members ?? []) {
    if (m.bitfield) continue;
    const inner = await peel(m.type.id);
    if (!m.name && (inner.kind === 'union' || inner.kind === 'struct')) {
      await scalars(inner, base + m.offset, out);      // flatten the anonymous member
    } else if (inner.kind === 'int') {
      out.push({ name: m.name, off: base + m.offset, size: inner.size, ...inner.encoding });
    }
  }
  return out;
}

console.log(await scalars(await btf.type('sock_common')));
```

### Discover an enum's values

```js
const e = await btf.type('bpf_map_type');
const byName = Object.fromEntries(e.values.map(v => [v.name, v.value]));
console.log(byName.BPF_MAP_TYPE_HASH);
```

### Disambiguate a name shared across kinds

```js
try {
  await btf.type('sigval');
} catch (e) {
  if (e.code === 'AMBIGUOUS_TYPE') {
    await btf.type('sigval', { kind: 'union' });
  }
}
```

### Query a target kernel other than the host

```sh
# start the daemon against a captured or shipped BTF
yeetd --btf-custom-path ./vmlinux-6.1-arm64.btf
```

```js
// queries now answer against that kernel's types, matching your .bpf.o CO-RE loads
const plan = await btf.walk('sk_buff', 'dev.name');
```
