SEA: BlobDeserializer SIGSEGVs when fuse byte is set but no NODE_SEA_BLOB is present
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 122k
- Forks
- 37.3k
- Avg merge
- 4d 2h
- Merged PRs (30d)
- 283
Description
Version
v26.1.0 (also reproduces on v25.6.0)
Platform
Linux arm64 (reproduced on Apple Silicon via Docker Desktop, but the SEGV is platform-independent)
Subsystem
sea
What steps will reproduce the bug?
Binaries where the postject fuse byte is set to 1 but NODE_SEA_BLOB cannot be located at runtime currently die with a NULL-deref SIGSEGV inside BlobDeserializer::ReadArithmetic, with no error message indicating the cause.
Take any Node binary, flip the fuse byte from 0 to 1 without injecting an actual SEA blob:
python3 -c "
sent = b'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'
with open('hello','rb') as f: buf = bytearray(f.read())
i = buf.find(sent)
buf[i + len(sent) + 1] = ord('1')
with open('hello','wb') as f: f.write(bytes(buf))
"
chmod +x hello
./hello --version # → Segmentation fault, exit 139
This state arises naturally when postject is run against a host binary with no PT_NOTE program header — postject silently fails to inject the note but still flips the fuse byte. See https://github.com/nodejs/postject/issues/107 and https://github.com/nodejs/unofficial-builds/issues/200.
How often does it reproduce? Is there a required condition?
100% reproducible. Required condition: fuse byte set to 1 AND no NODE_SEA_BLOB discoverable via postject_find_resource().
What is the expected behavior? Why is that the expected behavior?
A clear error indicating that the SEA fuse is set but no blob is present, rather than a bare SIGSEGV at startup. The current behavior makes it look like a crash in OpenSSL or libc (because the SIGILLs from OpenSSL's ARM crypto-extension probes show up first under gdb), when the actual cause is much earlier and recoverable.
What do you see instead?
Program received signal SIGSEGV, Segmentation fault.
#0 memcpy ()
#1 node::BlobDeserializer<...>::ReadArithmetic<unsigned int>()
#2 node::sea::FindSingleExecutableResource()
#3 node::sea::FixupArgsForSEA(int, char**)
#4 node::Start(int, char**)
postject_find_resource("NODE_SEA_BLOB", &size, ...) returns NULL, then BlobDeserializer::ReadArithmetic calls memcpy(dst, NULL, sizeof(uint32_t)) → SIGSEGV.
Additional information
Suggested fix in node::sea::FindSingleExecutableBlob() (src/node_sea_bin.cc) — guard the deserialization on the resource lookup:
const char* blob = static_cast<const char*>(
postject_find_resource("NODE_SEA_BLOB", &size, ...));
if (blob == nullptr) {
fprintf(stderr,
"node: SEA fuse is set but no NODE_SEA_BLOB resource was found "
"in this binary. The host binary may be missing a PT_NOTE program "
"header (run `readelf -lW <binary> | grep NOTE` to check).\n");
exit(static_cast<int>(node::ExitCode::kGenericUserError));
}
Either that or CHECK_NOT_NULL(blob) — anything that surfaces a cause rather than a bare SEGV.
Related:
- https://github.com/nodejs/unofficial-builds/issues/200 — upstream cause (arm64-musl tarball with no PT_NOTE)
- https://github.com/nodejs/unofficial-builds/pull/233 — fix for that root cause
- https://github.com/nodejs/postject/issues/107 — companion postject issue (silent injection failure)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/node_sea_bin.cc at node::sea::FindSingleExecutableBlob() and trace the postject_find_resource("NODE_SEA_BLOB", ...) result before BlobDeserializer runs. Reproduce with the supplied fuse-flipping command, then verify that a missing blob produces a clear startup error and nonzero exit instead of a SIGSEGV.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, linux, nodejs
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100