mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
dba8a31978
* 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>
164 lines
7.9 KiB
Markdown
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.
|