From 5334e502e2d6a1dfdedd693e06f1c663d19eb29e Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Tue, 11 Aug 2026 16:25:37 +0530 Subject: [PATCH] sea: error instead of SIGSEGV when fuse set without blob When the postject fuse byte is set but NODE_SEA_BLOB cannot be found, FindSingleExecutableBlob previously constructed a null string_view and BlobDeserializer NULL-dereferenced. Exit with a clear error instead. Fixes: https://github.com/nodejs/node/issues/63466 Signed-off-by: Sankalp Thakur --- src/node_sea_bin.cc | 19 +++++++ ...gle-executable-application-missing-blob.js | 52 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/sea/test-single-executable-application-missing-blob.js diff --git a/src/node_sea_bin.cc b/src/node_sea_bin.cc index ef5dab3a25c..4ca8ebebd54 100644 --- a/src/node_sea_bin.cc +++ b/src/node_sea_bin.cc @@ -20,6 +20,7 @@ #include "util-inl.h" #include +#include #include #include #include @@ -40,6 +41,7 @@ namespace node { namespace sea { +using node::ExitCode; // TODO(joyeecheung): use LIEF to locate it directly. std::string_view FindSingleExecutableBlob() { @@ -57,6 +59,23 @@ std::string_view FindSingleExecutableBlob() { const char* blob = static_cast( postject_find_resource("NODE_SEA_BLOB", &size, nullptr)); #endif + // Fuse set with no (or empty) blob used to NULL-deref in BlobDeserializer. + // See https://github.com/nodejs/node/issues/63466. + if (blob == nullptr || size == 0) { + char exec_path_buf[2 * PATH_MAX]; + size_t exec_path_len = sizeof(exec_path_buf); + const char* path = "this binary"; + if (uv_exepath(exec_path_buf, &exec_path_len) == 0) { + path = exec_path_buf; + } + FPrintF(stderr, + "node: SEA fuse is set but no valid NODE_SEA_BLOB resource " + "was found in %s.\n" + "The host binary may be missing a PT_NOTE program header " + "(run `readelf -lW | grep NOTE` to check).\n", + path); + exit(static_cast(ExitCode::kGenericUserError)); + } return {blob, size}; }(); per_process::Debug(DebugCategory::SEA, diff --git a/test/sea/test-single-executable-application-missing-blob.js b/test/sea/test-single-executable-application-missing-blob.js new file mode 100644 index 00000000000..831deb85268 --- /dev/null +++ b/test/sea/test-single-executable-application-missing-blob.js @@ -0,0 +1,52 @@ +'use strict'; + +// Verifies that a host binary with the SEA fuse set but without a +// NODE_SEA_BLOB resource exits with a clear error instead of SIGSEGV. +// Regression test for https://github.com/nodejs/node/issues/63466. + +require('../common'); + +const { + skipIfSingleExecutableIsNotSupported, + signSEA, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, readFileSync, writeFileSync, chmodSync } = require('fs'); +const { join } = require('path'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +tmpdir.refresh(); + +const fusedBinary = join(tmpdir.path, process.platform === 'win32' ? 'fused.exe' : 'fused'); +copyFileSync(process.execPath, fusedBinary); + +const fuse = Buffer.from('NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'); +const buf = readFileSync(fusedBinary); +const fuseAt = buf.indexOf(fuse); +if (fuseAt === -1) { + require('../common').skip('SEA fuse sentinel not found in process.execPath'); +} + +const fuseValueOffset = fuseAt + fuse.length + 1; // skip ':' +if (buf[fuseValueOffset] !== 0x30 /* '0' */) { + require('../common').skip(`Unexpected SEA fuse value: ${buf[fuseValueOffset]}`); +} + +buf[fuseValueOffset] = 0x31; // '1' +writeFileSync(fusedBinary, buf); +chmodSync(fusedBinary, 0o755); +signSEA(fusedBinary); + +spawnSyncAndAssert( + fusedBinary, + ['--version'], + {}, + { + status: 1, + signal: null, + stderr: /SEA fuse is set but no valid NODE_SEA_BLOB resource was found/, + }, +);