mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 06:46:10 +00:00
Artifact classification probes every ELF root for a Node SEA note. Each PT_NOTE program header was scanned on its own, one read per note, so headers that declare the same note bytes multiplied the work: a 295 KB ELF with 4,096 such headers kept inspect-artifact busy for about three and a half minutes. Every note header takes at least 12 bytes, so disjoint tables hold at most one note per 12 file bytes. The scan now stops there. Past that count the remaining tables are unreadable: an ELF without a confirmed blob keeps its native classification, and a confirmed blob fails closed.
114 lines
6.4 KiB
Markdown
114 lines
6.4 KiB
Markdown
# Node.js single executable applications
|
|
|
|
REA inventories injected Node.js SEA blobs through `inspect-artifact` /
|
|
`inspect_artifact` and materializes their contents through `extract-artifact` /
|
|
`extract_artifact`. It never runs the application or deserializes V8 snapshots
|
|
or code cache.
|
|
|
|
```sh
|
|
rea inspect-artifact ./application --json
|
|
rea inspect-artifact ./application --detail summary --evidence_output evidence.json --json
|
|
rea extract-artifact ./application --json
|
|
```
|
|
|
|
For MCP, open the carrier with `open_binary`, then call `inspect_artifact`.
|
|
Summary delivery retains the complete Evidence: use `parent_evidence_id` with
|
|
`inspect_analysis_view` to request particular inventory paths and fields.
|
|
Extraction returns a fresh `output_root`; analyze that extracted directory with
|
|
`analyze_javascript_application`. Snapshot-only applications have no recovered
|
|
main script and receive no JavaScript next probe. Neither does a main script
|
|
whose logical path lacks a JavaScript source suffix, such as `main`, because the
|
|
analyzer selects extracted files by suffix.
|
|
|
|
## Recognition and evidence
|
|
|
|
The container format is `node-sea`. REA locates Mach-O sections named
|
|
`__NODE_SEA_BLOB` in `NODE_SEA` or `__NODE_SEA`, ELF notes owned by
|
|
`NODE_SEA_BLOB`, and PE RCDATA resources named `NODE_SEA_BLOB`. PE resource
|
|
locations come from the shared PE resource parser. ELF program-header
|
|
`PT_NOTE` segments are authoritative, as at runtime; `SHT_NOTE` sections are
|
|
read only when no program-header table is readable, because injectors can
|
|
leave stale section entries. A PE without a `NODE_SEA_BLOB` resource name is
|
|
left to native classification. Universal Mach-O carriers
|
|
with different slice blobs are rejected as ambiguous; identical blobs are
|
|
inventoried once. Native carrier bytes keep their content hash and native
|
|
classification while the root occurrence represents the SEA container.
|
|
|
|
Root `format_metadata` preserves raw flags, decoded flags, code path, blob
|
|
location, exec arguments and fuse observations. Main and asset children retain
|
|
their role and exact declared name in `source_path`. Safe, noncolliding names
|
|
become logical paths after the shared NFC path normalization. Names that are
|
|
unsafe as relative paths, have an exact or file-prefix collision with another
|
|
entry, or occupy the reserved `__node_sea__`
|
|
namespace receive deterministic logical paths: `__node_sea__/main.cjs` for the
|
|
main CommonJS source and `__node_sea__/assets/<ordinal>` for assets, using their
|
|
zero-based order in the blob. The original declared name remains
|
|
in `source_path`, including an empty asset key. Snapshots and code caches use
|
|
`__node_sea__/snapshot` and `__node_sea__/code-cache`. Extraction materializes
|
|
the logical paths through the shared path-containment and collision rules and
|
|
never overwrites existing files.
|
|
|
|
The fuse scan independently records `set`, `unset`, `absent`, or `conflicting`.
|
|
An unset fuse with a present blob is reported as a limitation. These are static
|
|
sentinel observations, not proof of runtime activation: sentinel-like text in
|
|
script or asset bytes also participates in the scan.
|
|
|
|
Artifact comparison uses each member's runtime role and exact source key as
|
|
semantic metadata. Unicode-normalized graph paths do not erase differences in
|
|
runtime asset keys. For members paired at the same logical path, a directory
|
|
index change alone does not change the member. Ordinal-based fallback paths can
|
|
change when entries are reordered. When only one observation records member
|
|
metadata, the complete runtime member declaration remains unavailable. Original
|
|
source fields remain available in the retained Evidence.
|
|
|
|
## Supported layout and limits
|
|
|
|
The decoder follows the 64-bit little-endian layout in Node
|
|
[v24.10.0 node_sea.cc](https://github.com/nodejs/node/blob/v24.10.0/src/node_sea.cc)
|
|
and [node_sea.h](https://github.com/nodejs/node/blob/v24.10.0/src/node_sea.h),
|
|
including the exec-argument extension byte. Older layouts without that byte,
|
|
32-bit length fields, big-endian blobs and future flags are not supported.
|
|
Carrier byte order is independent of blob byte order.
|
|
|
|
Malformed magic, truncation, unknown flags/extensions, invalid UTF-8, duplicate
|
|
raw asset keys and conflicting blob declarations fail closed with named reasons.
|
|
Zero padding after a complete blob is allowed. The blob read ceiling is 256 MiB;
|
|
PE carriers also use the shared 512 MiB input, 65,536 directory-entry and 8 MiB
|
|
resource-metadata ceilings. The decoder admits at most 100,000 assets whose
|
|
names total at most 8 MiB, and 4,096 exec arguments totaling at most 1 MiB of
|
|
text. These are fixed resource bounds, not Node format limits, checked before
|
|
decoding names or creating records; a hit fails as a limit with no runtime
|
|
override. Asset and argument decoding, padding scans, and inventory construction
|
|
yield in bounded batches and honor caller cancellation. Asset count and name
|
|
budgets bound retained metadata independently of stored member body sizes. The PE
|
|
name probe reads headers from the first 64 KiB of the carrier; a PE whose
|
|
headers extend beyond that is left to native classification. ELF note tables
|
|
are read for at most one note per 12 carrier bytes in total, the most that
|
|
disjoint tables can hold, so program headers that repeat the same notes do not
|
|
rescan them once per header. Past that count the remaining tables are
|
|
unreadable: an ELF without a confirmed blob keeps its native classification,
|
|
and one with a blob fails closed. Snapshot and code-cache bytes remain opaque.
|
|
|
|
## Verification
|
|
|
|
Synthetic fixtures cover Mach-O, ELF and PE inventory/extraction, malformed
|
|
blobs, collisions and fuse contradictions. CLI and MCP tests exercise the same
|
|
artifact provider and advertised schemas. A CLI regression also generates a blob
|
|
with the pinned local Node version, an absolute main path and an empty asset
|
|
key, then inventories it inside a synthetic ELF carrier. This verifies the real
|
|
blob producer, not a real injected executable.
|
|
|
|
The optional real ELF lane uses Docker's official Node v24.10.0 image and pinned
|
|
postject to build a fixture, then verifies inventory and extracted script/asset
|
|
bytes without running the injected application:
|
|
|
|
```sh
|
|
npm run verify:node-sea
|
|
```
|
|
|
|
This requires Docker and network access to fetch the producer image and
|
|
postject. It creates and removes its own temporary fixture directory. A real
|
|
macOS Mach-O or Windows PE run is separate coverage; synthetic carriers do not
|
|
establish real-platform verification. No performance improvement is claimed;
|
|
whole-task tokens, CPU, memory, and cold/warm timings have not been measured.
|