Summary
With deadCodeInjection: true on already-minified input (bundler output where locals are 1-char identifiers, i.e. effectively the same as identifierNamesGenerator: 'mangled'), a positional argument is intermittently lost across a rest/spread forward ((...args) => fn(...args)): the callee receives one argument as undefined even though the caller passed a defined value.
The corruption is non-deterministic across builds from identical source, because the default seed: 0 re-randomises which AST fragments deadCodeInjection clones each run. The same source "works" in some builds and breaks in others.
This looks related to the historically-fixed scope/clone issues (#708, and the NodeUtils.cloneRecursive / range scope-resolution fix in v5.2.0), and to #262 (non-deterministic undefined / "is not a function" with deadCodeInjection + mangled + seed: 0), but I could not find an open issue with this exact signature (positional arg silently undefined through a spread forward).
Version
javascript-obfuscator: 5.4.3 (latest on npm)
- Node: v24.15.0
- Input: ESM chunk emitted by a bundler (rolldown/esbuild), already minified
Options
{
compact: true,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 0.75,
deadCodeInjection: true,
deadCodeInjectionThreshold: 0.4,
stringArray: true,
stringArrayEncoding: ['base64'],
stringArrayThreshold: 0.75,
identifierNamesGenerator: 'mangled',
selfDefending: false,
debugProtection: false,
// seed: 0 (default) — non-deterministic
}
What we observed
Our app dispatches IPC calls through a generic rest/spread forwarder. Source:
// for each registered method:
handleIpc(state, `${group}:${method}`, (e, ...args) =>
runWithRoute(routeFor(e.sender.id), searchParamsFor(e.sender.id), () =>
methods[method](...args), // <-- args forwarded via spread
),
);
Minified input handed to the obfuscator (unbroken, semantically correct):
(v,...b)=>qi(o(v.sender.id),a(v.sender.id),()=>p[h](...b))
Obfuscated output from a broken build (deadCodeInjection: true):
(H,...I)=>qi(w(H[auH(0x3932)]['id']),x(H[auH(0x3932)]['id']),()=>F[G](...I))
The structure looks fine, yet at runtime a call made with two defined arguments arrives at F[G] (p[h]) with the second argument undefined.
Concretely, we verified the boundary on both sides in the same broken build:
- Caller side (before the boundary), logged args:
["/path/to/repo", "5c09f59b-…"] — both defined.
- Callee side (after the spread forward):
arg0 = "/path/to/repo", arg1 = undefined.
Flipping deadCodeInjection: false (everything else unchanged) makes the problem disappear across every build we tried; pinning a non-zero seed makes the outcome reproducible per seed.
Why we can't attach a minimal standalone repro (yet)
We tried hard to reduce it. Obfuscating the isolated chunk that contains the forwarder — byte-for-byte the real bundler output — and executing it across 200 seeds produced zero corruptions. The failure only appears when the whole large chunk (~950 KB, our Electron main-process bundle) is obfuscated as one unit. That strongly suggests the corruption is an emergent property of deadCodeInjection cloning fragments from elsewhere in the same chunk (it "steals a part of code" per #708), so a small extracted snippet has nothing to steal and never triggers it. The full bundle isn't runnable standalone (native requires), which is what has blocked a clean minimal repro.
We understand a non-reproducible report is hard to action. We're filing it because the signature (silent undefined through a spread forward, non-deterministic by seed, only with deadCodeInjection + mangled/minified input) seems like a real miscompile and may ring a bell given #262/#708/v5.2.0.
Offer to help
We can run any instrumentation / logging / debug build you suggest against our real bundle and report back, and we're happy to iterate to help you pin it down (e.g. bisecting seeds, dumping the pre/post AST for a specific node, trying a patched build). If a private channel would be easier for sharing a larger reproduction artifact, we can arrange that too. Just let us know what would be most useful.
Workaround (for anyone hitting this)
Disable deadCodeInjection (it adds little real reverse-engineering resistance) and/or pin a fixed non-zero seed so builds are reproducible and a latent miscompile can't surface only in some releases.
Summary
With
deadCodeInjection: trueon already-minified input (bundler output where locals are 1-char identifiers, i.e. effectively the same asidentifierNamesGenerator: 'mangled'), a positional argument is intermittently lost across a rest/spread forward ((...args) => fn(...args)): the callee receives one argument asundefinedeven though the caller passed a defined value.The corruption is non-deterministic across builds from identical source, because the default
seed: 0re-randomises which AST fragmentsdeadCodeInjectionclones each run. The same source "works" in some builds and breaks in others.This looks related to the historically-fixed scope/clone issues (#708, and the
NodeUtils.cloneRecursive/rangescope-resolution fix in v5.2.0), and to #262 (non-deterministicundefined/ "is not a function" withdeadCodeInjection+mangled+seed: 0), but I could not find an open issue with this exact signature (positional arg silentlyundefinedthrough a spread forward).Version
javascript-obfuscator: 5.4.3 (latest on npm)Options
What we observed
Our app dispatches IPC calls through a generic rest/spread forwarder. Source:
Minified input handed to the obfuscator (unbroken, semantically correct):
Obfuscated output from a broken build (
deadCodeInjection: true):The structure looks fine, yet at runtime a call made with two defined arguments arrives at
F[G](p[h]) with the second argumentundefined.Concretely, we verified the boundary on both sides in the same broken build:
["/path/to/repo", "5c09f59b-…"]— both defined.arg0 = "/path/to/repo",arg1 = undefined.Flipping
deadCodeInjection: false(everything else unchanged) makes the problem disappear across every build we tried; pinning a non-zeroseedmakes the outcome reproducible per seed.Why we can't attach a minimal standalone repro (yet)
We tried hard to reduce it. Obfuscating the isolated chunk that contains the forwarder — byte-for-byte the real bundler output — and executing it across 200 seeds produced zero corruptions. The failure only appears when the whole large chunk (~950 KB, our Electron main-process bundle) is obfuscated as one unit. That strongly suggests the corruption is an emergent property of
deadCodeInjectioncloning fragments from elsewhere in the same chunk (it "steals a part of code" per #708), so a small extracted snippet has nothing to steal and never triggers it. The full bundle isn't runnable standalone (nativerequires), which is what has blocked a clean minimal repro.We understand a non-reproducible report is hard to action. We're filing it because the signature (silent
undefinedthrough a spread forward, non-deterministic by seed, only withdeadCodeInjection+ mangled/minified input) seems like a real miscompile and may ring a bell given #262/#708/v5.2.0.Offer to help
We can run any instrumentation / logging / debug build you suggest against our real bundle and report back, and we're happy to iterate to help you pin it down (e.g. bisecting seeds, dumping the pre/post AST for a specific node, trying a patched build). If a private channel would be easier for sharing a larger reproduction artifact, we can arrange that too. Just let us know what would be most useful.
Workaround (for anyone hitting this)
Disable
deadCodeInjection(it adds little real reverse-engineering resistance) and/or pin a fixed non-zeroseedso builds are reproducible and a latent miscompile can't surface only in some releases.