Skip to content

Commit 974df53

Browse files
robertsLandoclaude
andauthored
fix(sea): silence benign LIEF warnings during postject injection (#265)
* fix(sea): silence benign LIEF warnings during postject injection LIEF (inside postject) prints "signature seems corrupted" and "Can't find string offset for section name '.note.100'" to stderr after postject expands the ELF section table to make room for NODE_SEA_BLOB. The messages are cosmetic — the injection succeeds and macOS binaries are re-signed afterwards — but users reasonably assume something is wrong. Wrap the postject.inject() call in a tiny stderr filter that drops only those specific lines. Everything else passes through unchanged, and the original process.stderr.write is restored in a finally block. * refactor(sea): tighten typing of postject stderr filter Replace `unknown` + `...rest: unknown[]` (which forced two `as` casts and a runtime `rest.find` to locate the callback) with the actual write() overload parameters: `chunk: string | Uint8Array`, `encodingOrCb?: BufferEncoding | WriteCallback`, `cb?: WriteCallback`. Disambiguate the pass-through call so the right write() overload is dispatched, and handle plain Uint8Array chunks explicitly. Whitelist `BufferEncoding` as a global in the TS ESLint block, mirroring how `NodeJS` is already handled — both are type-only identifiers that `no-undef` can't resolve on its own. * fix(sea): restore original stderr.write reference, not a bound wrapper Keep the unbound write function for restoration so process.stderr.write regains its exact prior identity after the call. Use a separate bound copy internally for calling from the filter. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent a8e91df commit 974df53

2 files changed

Lines changed: 69 additions & 5 deletions

File tree

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ module.exports = [
8080
globals: {
8181
...require('globals').node,
8282
NodeJS: 'readonly',
83+
BufferEncoding: 'readonly',
8384
},
8485
},
8586
rules: {

lib/sea.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,67 @@ const exists = async (path: string) => {
6262
}
6363
};
6464

65+
/**
66+
* Benign LIEF messages printed by postject during SEA blob injection.
67+
*
68+
* LIEF re-parses the Node binary after postject expands the section
69+
* table to make room for `NODE_SEA_BLOB`, and the pre-existing
70+
* build-id / `.note.*` section-name offsets no longer resolve cleanly
71+
* through `.shstrtab` — so LIEF falls back to synthetic names like
72+
* `.note.100` and warns. On Mach-O the analogous "signature seems
73+
* corrupted" line is also cosmetic: we re-sign the binary with
74+
* `codesign` after injection (see signMachOExecutable).
75+
*
76+
* The warnings have no bearing on correctness of the produced
77+
* executable but users reasonably assume something is wrong, so we
78+
* filter them out of stderr just for the duration of the inject call.
79+
*/
80+
const BENIGN_POSTJECT_STDERR =
81+
/^warning: (?:The signature seems corrupted!|Can't find string offset for section name '\.note)/;
82+
83+
type StderrWrite = typeof process.stderr.write;
84+
type WriteCallback = (err?: Error | null) => void;
85+
86+
/**
87+
* Run `fn` with `process.stderr.write` wrapped to drop known-benign
88+
* postject/LIEF messages. Anything that doesn't match the allow-list
89+
* pattern passes through unchanged, so real errors are never hidden.
90+
* The original `write` is restored in a `finally` block regardless of
91+
* whether `fn` resolves or rejects.
92+
*/
93+
async function withFilteredPostjectStderr<T>(fn: () => Promise<T>): Promise<T> {
94+
const original: StderrWrite = process.stderr.write;
95+
const bound: StderrWrite = original.bind(process.stderr);
96+
const filtered = ((
97+
chunk: string | Uint8Array,
98+
encodingOrCb?: BufferEncoding | WriteCallback,
99+
cb?: WriteCallback,
100+
): boolean => {
101+
const text =
102+
typeof chunk === 'string'
103+
? chunk
104+
: Buffer.isBuffer(chunk)
105+
? chunk.toString('utf8')
106+
: Buffer.from(chunk).toString('utf8');
107+
if (BENIGN_POSTJECT_STDERR.test(text)) {
108+
const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
109+
if (callback) process.nextTick(callback);
110+
return true;
111+
}
112+
// The write() overloads accept either an encoding or a callback in
113+
// slot 2; disambiguate here so the correct overload is dispatched.
114+
return typeof encodingOrCb === 'function'
115+
? bound(chunk, encodingOrCb)
116+
: bound(chunk, encodingOrCb, cb);
117+
}) as StderrWrite;
118+
process.stderr.write = filtered;
119+
try {
120+
return await fn();
121+
} finally {
122+
process.stderr.write = original;
123+
}
124+
}
125+
65126
export type GetNodejsExecutableOptions = {
66127
useLocalNode?: boolean;
67128
nodePath?: string;
@@ -398,11 +459,13 @@ async function bake(
398459
// This avoids two CI issues:
399460
// 1. "Text file busy" race condition from concurrent npx invocations
400461
// 2. "Argument is not a constructor" from npx downloading incompatible versions
401-
await postjectInject(outPath, 'NODE_SEA_BLOB', blobData, {
402-
sentinelFuse: SEA_SENTINEL_FUSE,
403-
machoSegmentName: target.platform === 'macos' ? 'NODE_SEA' : undefined,
404-
overwrite: true,
405-
});
462+
await withFilteredPostjectStderr(() =>
463+
postjectInject(outPath, 'NODE_SEA_BLOB', blobData, {
464+
sentinelFuse: SEA_SENTINEL_FUSE,
465+
machoSegmentName: target.platform === 'macos' ? 'NODE_SEA' : undefined,
466+
overwrite: true,
467+
}),
468+
);
406469
}
407470

408471
/**

0 commit comments

Comments
 (0)