Files
rea/docs/kernel-modules.md
T
Matt Van Horn dba8a31978 feat(elf): inspect Linux kernel module metadata (#7010)
* feat(elf): inspect Linux kernel module metadata

Adds inspect-kernel-module and inspect_kernel_module for ELF32/ELF64
ET_REL Linux kernel modules, read statically without executing them.
Reports ordered .modinfo entries with duplicates preserved, __versions
symbol CRCs, the presence of .gnu.linkonce.this_module and __ksymtab,
.init.text and .exit.text sizes, and an appended module signature
trailer with its PKCS#7 byte range and SHA-256. gzip, xz and zstd
containers are reported unsupported with external decompression as the
next probe. Malformed headers and exhausted entry budgets return no
partial success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(elf): bound kernel module ksymtab listing and tighten contracts

Charge listed __ksymtab* sections against max_entries, omit section names
longer than 256 bytes with ksymtab_name_exceeds_limit, and aggregate the
non-file-backed limitation so repeated huge names cannot inflate Evidence.
Use fixedTupleSchema for the signature pad bytes with projection parity
tests, and assert the late-cancellation reason and retained observation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix: resolve merge integration errors

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-24 22:35:20 +08:00

164 lines
7.9 KiB
Markdown

# Linux kernel modules
`inspect_kernel_module` / `inspect-kernel-module` reads metadata from one
selected local ELF32/ELF64 relocatable object (`.ko`) without executing it.
That metadata is the `.modinfo` key/value strings, the `__versions` symbol
CRCs, the presence of `.gnu.linkonce.this_module` and `__ksymtab*` sections,
the observed file sizes of `.init.text` and `.exit.text`, and an optional
appended signature trailer. It does not require a Linux host or the `modinfo`
program.
```bash
rea inspect-kernel-module /absolute/path/to/module.ko --json
```
The CLI resolves relative paths against the caller's working directory. MCP
requires an absolute filesystem path and preserves that exact selection:
```json
{
"name": "inspect_kernel_module",
"arguments": { "path": "/artifacts/demo.ko" }
}
```
## Observations
The inline Evidence result contains the artifact path, SHA-256, and byte
length. `applicability` is `applicable` only for `ET_REL`. Other ELF types are
`not_applicable`: header facts are filled and `.modinfo`, `__versions`, and
the signature trailer are not examined.
`.modinfo` entries stay in file order. Duplicate keys such as `alias` and
`parm` are preserved. Each entry keeps its key, value, file range, and
SHA-256. Values are the bytes after the first `=`. NUL padding between
strings is skipped.
`__versions` entries are `struct modversion_info`: a 64-byte record whose CRC
is an `unsigned long` and whose name occupies the remaining bytes. ELF32
stores a 32-bit CRC and a 60-byte name. ELF64 stores a 64-bit unsigned long
and a 56-byte name. CRCs are zero-padded lowercase hex, 8 or 16 characters,
not a JavaScript number. The layout is reported from the ELF class even when
the section is absent.
`.gnu.linkonce.this_module` is reported as present, absent, or unknown.
`layout` is always `unknown` because `struct module` changes with the kernel
version and is not decoded. File-backed `__ksymtab*` sections are listed in
section-header order by name and file range; their records are not decoded.
Sections that are `SHT_NOBITS` or leave the file are counted in one limitation
without their names. `.init.text` and `.exit.text` report the
observed file-backed size. A compressed payload reports the stored size and a
limitation that the decoded size is unknown.
The signature trailer is the 27-byte magic `~Module signature appended~\n`
preceded by a 12-byte `module_signature` (`algo`, `hash`, `id_type`,
`signer_len`, `key_id_len`, three pad bytes, and big-endian `sig_len`). When
those parameter fields are zero and `id_type` is PKCS#7 (`2`), the preceding
`sig_len` bytes are the PKCS#7 range and its SHA-256. The range is not parsed
and no certificate, signer, or digest-coverage claim is made. An absent
trailer is observed absence, not proof the module is unsigned.
When section names are incomplete, a missing named section is `unknown`
rather than `absent`. An empty file-backed section is present and empty.
## Limits and failures
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
8192 `.modinfo` and `__versions` entries plus listed `__ksymtab*` sections are
examined by default (65536 maximum); `coverage.examined_entries` counts all
three. Exceeding that budget fails with no partial success. Keys longer
than 256 bytes and values longer than 4096 bytes stay malformed per entry;
the field that fits is still copied. A `__ksymtab*` section name longer than
256 bytes keeps its range with `name: null` and `ksymtab_name_exceeds_limit`. Inline raw hex stops at 4096 bytes and
the SHA-256 remains.
A `.modinfo` span with no trailing NUL is `unterminated_modinfo_entry` and
earlier entries are kept. A string with no `=` is `modinfo_missing_separator`.
An empty key is `modinfo_empty_key`. A `__versions` name with no NUL is
`version_name_unterminated`. A `__versions` section whose size is not a
multiple of 64 bytes is closed with `versions_size_not_multiple` and no
entries. `SHT_NOBITS` or a declared range that leaves the file is
`modinfo_section_not_file_backed` or `versions_section_not_file_backed`.
`SHF_COMPRESSED` payloads are not inflated
(`modinfo_section_compressed`, `versions_section_compressed`).
An appended magic with fewer than 12 preceding bytes is
`signature_trailer_truncated`. A `sig_len` that does not leave at least one
module byte is `signature_length_inconsistent`. Nonzero `algo`, `hash`,
signer, key-id, or pad fields for PKCS#7 are `signature_parameters_nonzero`.
A parameter block whose `id_type` is not PKCS#7 is
`signature_id_type_not_pkcs7` and is unsupported rather than a validated
signature.
gzip (`1f 8b`), xz (`fd 37 7a 58 5a 00`), and zstd (`28 b5 2f fd`) whole-file
containers are `unsupported`. The result names `external_decompression` as the
next probe and does not claim the payload is a kernel module. This is an external
step, not a REA operation: neither this tool nor `extract_artifact` decodes these
containers. Use an external gzip, xz, or zstd utility to decompress into a separate
file, then call `inspect_kernel_module` on that file to inspect the `ET_REL` image.
Malformed ELF headers return no partial kernel-module metadata. This workflow
does not execute the module or validate an appended signature.
## Format references
The record layout follows Linux v6.12's
[`modversion_info`](https://github.com/torvalds/linux/blob/v6.12/include/linux/module.h)
and [`MAX_PARAM_PREFIX_LEN`](https://github.com/torvalds/linux/blob/v6.12/include/linux/moduleparam.h).
Trailer fields and acceptance rules follow
[`module_signature`](https://github.com/torvalds/linux/blob/v6.12/include/linux/module_signature.h)
and [`mod_check_sig`](https://github.com/torvalds/linux/blob/v6.12/kernel/module_signature.c).
These references establish the decoded layout, not runtime verification.
## Real-module verification lane
Run from a built checkout with Docker available. This downloads a Debian kernel
package into a disposable container, extracts a filesystem module, decompresses
it when necessary, and inspects it with REA. It never installs or loads that
kernel module. The temporary output is outside the repository; record the
printed package version and module digest with results.
```bash
module_lane=$(mktemp -d)
docker run --rm -i -v "$module_lane:/out" debian:stable bash -se <<'CONTAINER'
apt-get update -qq
apt-get install -y --no-install-recommends xz-utils zstd ca-certificates
cd /tmp
arch=$(dpkg --print-architecture)
package=$(apt-cache depends "linux-image-$arch" | awk '/Depends: linux-image-[0-9]/{print $2; exit}')
test -n "$package"
apt-get download "$package"
dpkg-deb -f ./*.deb Package Version > /out/package.txt
dpkg-deb -x ./*.deb /tmp/kernel
module=$(find /tmp/kernel -type f -path '*/kernel/fs/*' \( -name '*.ko' -o -name '*.ko.xz' -o -name '*.ko.zst' -o -name '*.ko.gz' \) | sort | head -1)
test -n "$module"
case "$module" in
*.xz) xz -dc "$module" > /out/module.ko ;;
*.zst) zstd -dc "$module" > /out/module.ko ;;
*.gz) gzip -dc "$module" > /out/module.ko ;;
*) cp "$module" /out/module.ko ;;
esac
cat /out/package.txt
sha256sum /out/module.ko
CONTAINER
node scripts/rea.mjs inspect-kernel-module "$module_lane/module.ko" --json > "$module_lane/rea.json"
python3 - "$module_lane" <<'PY'
import json, pathlib, sys
root = pathlib.Path(sys.argv[1])
report = json.loads((root / 'rea.json').read_text())['normalized_result']
entries = report['modinfo']['entries']
assert entries and all(entry['status'] == 'observed' for entry in entries)
keys = [entry['key'] for entry in entries]
for key in ('license', 'vermagic', 'name'):
assert key in keys, key
print('Observed .modinfo entries:', len(entries), 'distinct keys:', len(set(keys)))
print('Artifact:', report['artifact'])
PY
```
`parm` and `parmtype` are retained separately by REA. Synthetic tests cover
their generic key/value representation and all four ELF class/byte-order
combinations. A successful real lane verifies only the selected module and
printed fields; it does not validate certificates. This lane has not run in the
sandbox used for implementation because access to the Docker socket was denied.