[GH-ISSUE #3926] [Bug] Unavailable ASAR native add-ons inherit the archive digest in feature traces #1534

Closed
opened 2026-10-16 21:06:49 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3926

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

Tracing a native add-on whose unpacked ASAR companion is missing returns a handoff that names the add-on path but uses the containing archive's SHA-256. Supplying simulated native-provider Evidence for the archive also marks this unavailable add-on as evidence-linked. The analysis graph correctly records that the add-on bytes and content identity are unavailable, but the feature trace does not preserve that distinction.

Observed analysis and both trace commands exited 0. The analysis retained a native-addon node with canonical-path identity, availability: "unavailable", hash_status: "unavailable", and inventory_artifact_id: null. Its observation identified the ASAR as the available evidence artifact. The trace nevertheless reported artifact_path: "native/addon.node" and artifact_sha256: "f7146f6509ee18031bd9549becabd6a4bfb2ac24e8e4861a53f6c13ee7a79ecd", exactly matching the independently calculated archive digest.

Without native observations, status was requires-provider-analysis. With simulated container-subject Ghidra Evidence, status became evidence-linked, and the handoff included that provider and Evidence ID. This establishes incorrect linkage through the CLI with simulated Evidence, not real-provider analysis behavior.

As an executed control, restoring app.asar.unpacked/native/addon.node from source/native/addon.node, repeating analysis, and tracing with the same container-subject Evidence produced a content-digest native node and handoff SHA-256 3d7410865c8c14aba60c7a1a77fbabf2b50c8575b913a15579282f75754a13a9. That digest matched the add-on bytes. The control reported requires-provider-analysis with empty providers and Evidence IDs.

Reproduction

Run from the baseline checkout with dependencies installed and the runtime built. The commands create disposable input and isolate HOME. This fixture and the missing-byte trace were executed once, followed by the simulated-link trace and available-byte control below.

  1. Create an ASAR with an unpacked native add-on, then remove its companion bytes:
export REPRO_ROOT="$(mktemp -d)"
export HOME="$REPRO_ROOT/home"
mkdir -p "$HOME"
node --input-type=module <<'JS'
import { mkdir, writeFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { createPackageWithOptions } from '@electron/asar';
const root = process.env.REPRO_ROOT;
const source = join(root, 'source');
await mkdir(join(source, 'native'), { recursive: true });
await writeFile(join(source, 'main.js'), 'module.exports = "observed";\n');
await writeFile(join(source, 'native/addon.node'), 'fixture native addon bytes');
await createPackageWithOptions(source, join(root, 'app.asar'), { unpack: '*.node' });
await rm(join(root, 'app.asar.unpacked/native/addon.node'));
JS
node scripts/rea.mjs analyze-javascript-application "$REPRO_ROOT/app.asar" --json > "$REPRO_ROOT/analysis.json"
  1. Trace the add-on path using the complete returned Evidence:
node --input-type=module <<'JS'
import { readFile, writeFile } from 'node:fs/promises';
const root = process.env.REPRO_ROOT;
const application = JSON.parse(await readFile(root + '/analysis.json', 'utf8'));
await writeFile(root + '/trace-input.json', JSON.stringify({
  application,
  seed: { kind: 'string', value: 'native/addon.node', match: 'exact' },
  direction: 'both'
}));
JS
node scripts/rea.mjs trace-application-feature "$REPRO_ROOT/trace-input.json" --json > "$REPRO_ROOT/trace.json"
node --input-type=module <<'JS'
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
const root = process.env.REPRO_ROOT;
const analysis = JSON.parse(await readFile(root + '/analysis.json', 'utf8'));
const trace = JSON.parse(await readFile(root + '/trace.json', 'utf8'));
console.log('container_sha256', createHash('sha256').update(await readFile(root + '/app.asar')).digest('hex'));
console.log('native_nodes', JSON.stringify(analysis.normalized_result.graph.nodes.filter(n => n.kind === 'native-addon')));
console.log('native_handoffs', JSON.stringify(trace.normalized_result.native_handoffs));
JS
  1. To reproduce the incorrect linkage, supply simulated container-subject Evidence and run the same public trace command:
node --input-type=module <<'JS'
import { readFile, writeFile } from 'node:fs/promises';
import { createEvidence } from './dist/domain/evidence.js';
const root = process.env.REPRO_ROOT;
const input = JSON.parse(await readFile(root + '/trace-input.json', 'utf8'));
input.native_observations = [createEvidence(
  { path: root + '/app.asar', sha256: input.application.subject.digest.sha256, format: 'raw-binary', architecture: null },
  { id: 'ghidra', name: 'Ghidra', version: 'fixture' },
  { operation: 'binary_overview', parameters: {}, result: { summary: 'Synthetic container-subject native provider observation' }, limitations: ['Simulated native Evidence; no native provider executed.'] }
)];
await writeFile(root + '/linked-input.json', JSON.stringify(input));
JS
node scripts/rea.mjs trace-application-feature "$REPRO_ROOT/linked-input.json" --json > "$REPRO_ROOT/linked-trace.json"

Inspect normalized_result.native_handoffs in each trace result. Step 2 reports the archive digest for native/addon.node; step 3 also reports evidence-linked with the simulated provider and Evidence ID.

Expected behavior

Missing unpacked companion bytes are a supported partial-analysis case. Their content identity must remain unknown. A native handoff must bind the exact native artifact digest, and native-provider Evidence must match that artifact. The ASAR's digest and Evidence must not be attributed to an unavailable add-on. The documented contract does not prescribe a particular representation for an unavailable handoff.

Impact and scope

A supported partial-analysis input produces a native handoff with a different artifact's identity and can attach simulated provider Evidence for that artifact. This can misdirect downstream native analysis or overstate evidence linkage. The demonstrated failure is bounded to unavailable companion bytes; no security boundary crossing or real-provider execution was demonstrated.

Workaround: The returned graph retains availability and hash_status as unavailable for the affected native node. Callers can inspect those fields and disregard its handoff digest and linked Evidence while retaining the rest of the partial analysis; the available-byte control also demonstrated correct digest binding when the original companion bytes are supplied.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

ASAR with an inventoried unpacked native/addon.node whose companion bytes are missing. Native-provider Evidence is explicitly simulated.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/javascript-application-workflows.md:45-56 promises exact native artifact digest binding and matching native Evidence; docs/javascript-artifact-reconstruction.md:158-162 explicitly supports unavailable unpacked companion bytes without a content-addressed child artifact; AGENTS.md, Architecture and Cleanup, requires preserving identity and uncertainty across boundaries.
  • Failure source: Observed CLI analysis and trace results from the steps above. Source-derived explanation: src/application/javascript/JavaScriptArtifactGraphStructure.ts:370-405 creates an unavailable canonical-path native node with container-backed evidence; src/domain/javascript/javascriptNativeHandoff.ts:57-64 selects that observation's available artifact digest, and src/domain/javascript/javascriptNativeHandoff.ts:19-37 emits it and matches supplied Evidence against it.
  • An existing CLI test at tests/boundary/cli/javascriptSelfImportRelationships.test.ts:148-171 independently documents retaining an unavailable native add-on when its companion is missing. It was read as supporting source evidence, not run as part of this verification.
  • The current handoff schema requires a digest at src/domain/javascript/javascriptFeatureTraceSchemas.ts:132-140; this report does not require returning a handoff that omits a currently required field.

Typed outcome or recovery expected

Repeat the missing-companion fixture and trace, both without native observations and with the simulated container-subject Evidence. Passing behavior preserves the missing add-on's unknown content identity, never reports the archive digest as the add-on digest, and never links the archive's Evidence as analysis of that add-on. Keep the unavailable add-on visible in the application graph. Restore the original companion bytes and repeat analysis and tracing as a control: an available add-on must still produce a handoff bound to its own byte digest, while the same container-subject Evidence stays unlinked.

Analysis or suspected cause

Source-derived explanation and exact locators are in Relevant output or evidence. Provider and transport limits are stated there separately from reproduced results.

Additional context

Related closed issue #3213 and open PR #3278 concern retaining unavailable companion entries and import resolution. This report concerns native-handoff identity and Evidence linkage after the unavailable entry is retained.

Freshness check: the cited source and contract files have no changes between the tested source commit and upstream main at 22ec8bdb2f.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3926 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior Tracing a native add-on whose unpacked ASAR companion is missing returns a handoff that names the add-on path but uses the containing archive's SHA-256. Supplying simulated native-provider Evidence for the archive also marks this unavailable add-on as evidence-linked. The analysis graph correctly records that the add-on bytes and content identity are unavailable, but the feature trace does not preserve that distinction. Observed analysis and both trace commands exited 0. The analysis retained a native-addon node with canonical-path identity, `availability: "unavailable"`, `hash_status: "unavailable"`, and `inventory_artifact_id: null`. Its observation identified the ASAR as the available evidence artifact. The trace nevertheless reported `artifact_path: "native/addon.node"` and `artifact_sha256: "f7146f6509ee18031bd9549becabd6a4bfb2ac24e8e4861a53f6c13ee7a79ecd"`, exactly matching the independently calculated archive digest. Without native observations, status was `requires-provider-analysis`. With simulated container-subject Ghidra Evidence, status became `evidence-linked`, and the handoff included that provider and Evidence ID. This establishes incorrect linkage through the CLI with simulated Evidence, not real-provider analysis behavior. As an executed control, restoring `app.asar.unpacked/native/addon.node` from `source/native/addon.node`, repeating analysis, and tracing with the same container-subject Evidence produced a content-digest native node and handoff SHA-256 `3d7410865c8c14aba60c7a1a77fbabf2b50c8575b913a15579282f75754a13a9`. That digest matched the add-on bytes. The control reported `requires-provider-analysis` with empty providers and Evidence IDs. ### Reproduction Run from the baseline checkout with dependencies installed and the runtime built. The commands create disposable input and isolate HOME. This fixture and the missing-byte trace were executed once, followed by the simulated-link trace and available-byte control below. 1. Create an ASAR with an unpacked native add-on, then remove its companion bytes: ```sh export REPRO_ROOT="$(mktemp -d)" export HOME="$REPRO_ROOT/home" mkdir -p "$HOME" node --input-type=module <<'JS' import { mkdir, writeFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { createPackageWithOptions } from '@electron/asar'; const root = process.env.REPRO_ROOT; const source = join(root, 'source'); await mkdir(join(source, 'native'), { recursive: true }); await writeFile(join(source, 'main.js'), 'module.exports = "observed";\n'); await writeFile(join(source, 'native/addon.node'), 'fixture native addon bytes'); await createPackageWithOptions(source, join(root, 'app.asar'), { unpack: '*.node' }); await rm(join(root, 'app.asar.unpacked/native/addon.node')); JS node scripts/rea.mjs analyze-javascript-application "$REPRO_ROOT/app.asar" --json > "$REPRO_ROOT/analysis.json" ``` 2. Trace the add-on path using the complete returned Evidence: ```sh node --input-type=module <<'JS' import { readFile, writeFile } from 'node:fs/promises'; const root = process.env.REPRO_ROOT; const application = JSON.parse(await readFile(root + '/analysis.json', 'utf8')); await writeFile(root + '/trace-input.json', JSON.stringify({ application, seed: { kind: 'string', value: 'native/addon.node', match: 'exact' }, direction: 'both' })); JS node scripts/rea.mjs trace-application-feature "$REPRO_ROOT/trace-input.json" --json > "$REPRO_ROOT/trace.json" node --input-type=module <<'JS' import { readFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; const root = process.env.REPRO_ROOT; const analysis = JSON.parse(await readFile(root + '/analysis.json', 'utf8')); const trace = JSON.parse(await readFile(root + '/trace.json', 'utf8')); console.log('container_sha256', createHash('sha256').update(await readFile(root + '/app.asar')).digest('hex')); console.log('native_nodes', JSON.stringify(analysis.normalized_result.graph.nodes.filter(n => n.kind === 'native-addon'))); console.log('native_handoffs', JSON.stringify(trace.normalized_result.native_handoffs)); JS ``` 3. To reproduce the incorrect linkage, supply simulated container-subject Evidence and run the same public trace command: ```sh node --input-type=module <<'JS' import { readFile, writeFile } from 'node:fs/promises'; import { createEvidence } from './dist/domain/evidence.js'; const root = process.env.REPRO_ROOT; const input = JSON.parse(await readFile(root + '/trace-input.json', 'utf8')); input.native_observations = [createEvidence( { path: root + '/app.asar', sha256: input.application.subject.digest.sha256, format: 'raw-binary', architecture: null }, { id: 'ghidra', name: 'Ghidra', version: 'fixture' }, { operation: 'binary_overview', parameters: {}, result: { summary: 'Synthetic container-subject native provider observation' }, limitations: ['Simulated native Evidence; no native provider executed.'] } )]; await writeFile(root + '/linked-input.json', JSON.stringify(input)); JS node scripts/rea.mjs trace-application-feature "$REPRO_ROOT/linked-input.json" --json > "$REPRO_ROOT/linked-trace.json" ``` Inspect `normalized_result.native_handoffs` in each trace result. Step 2 reports the archive digest for `native/addon.node`; step 3 also reports `evidence-linked` with the simulated provider and Evidence ID. ### Expected behavior Missing unpacked companion bytes are a supported partial-analysis case. Their content identity must remain unknown. A native handoff must bind the exact native artifact digest, and native-provider Evidence must match that artifact. The ASAR's digest and Evidence must not be attributed to an unavailable add-on. The documented contract does not prescribe a particular representation for an unavailable handoff. ### Impact and scope A supported partial-analysis input produces a native handoff with a different artifact's identity and can attach simulated provider Evidence for that artifact. This can misdirect downstream native analysis or overstate evidence linkage. The demonstrated failure is bounded to unavailable companion bytes; no security boundary crossing or real-provider execution was demonstrated. Workaround: The returned graph retains availability and hash_status as unavailable for the affected native node. Callers can inspect those fields and disregard its handoff digest and linked Evidence while retaining the rest of the partial analysis; the available-byte control also demonstrated correct digest binding when the original companion bytes are supplied. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context ASAR with an inventoried unpacked native/addon.node whose companion bytes are missing. Native-provider Evidence is explicitly simulated. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: `docs/javascript-application-workflows.md:45-56` promises exact native artifact digest binding and matching native Evidence; `docs/javascript-artifact-reconstruction.md:158-162` explicitly supports unavailable unpacked companion bytes without a content-addressed child artifact; `AGENTS.md`, Architecture and Cleanup, requires preserving identity and uncertainty across boundaries. - Failure source: Observed CLI analysis and trace results from the steps above. Source-derived explanation: `src/application/javascript/JavaScriptArtifactGraphStructure.ts:370-405` creates an unavailable canonical-path native node with container-backed evidence; `src/domain/javascript/javascriptNativeHandoff.ts:57-64` selects that observation's available artifact digest, and `src/domain/javascript/javascriptNativeHandoff.ts:19-37` emits it and matches supplied Evidence against it. - An existing CLI test at `tests/boundary/cli/javascriptSelfImportRelationships.test.ts:148-171` independently documents retaining an unavailable native add-on when its companion is missing. It was read as supporting source evidence, not run as part of this verification. - The current handoff schema requires a digest at `src/domain/javascript/javascriptFeatureTraceSchemas.ts:132-140`; this report does not require returning a handoff that omits a currently required field. ### Typed outcome or recovery expected Repeat the missing-companion fixture and trace, both without native observations and with the simulated container-subject Evidence. Passing behavior preserves the missing add-on's unknown content identity, never reports the archive digest as the add-on digest, and never links the archive's Evidence as analysis of that add-on. Keep the unavailable add-on visible in the application graph. Restore the original companion bytes and repeat analysis and tracing as a control: an available add-on must still produce a handoff bound to its own byte digest, while the same container-subject Evidence stays unlinked. ### Analysis or suspected cause Source-derived explanation and exact locators are in Relevant output or evidence. Provider and transport limits are stated there separately from reproduced results. ### Additional context Related closed issue #3213 and open PR #3278 concern retaining unavailable companion entries and import resolution. This report concerns native-handoff identity and Evidence linkage after the unavailable entry is retained. Freshness check: the cited source and contract files have no changes between the tested source commit and upstream main at 22ec8bdb2f7d4626587f23b1eed4a207df600386.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Confirmed: an unavailable native add-on must not inherit its containing archive digest — the analysis already knows the add-on bytes are unavailable (hash_status unavailable, inventory_artifact_id null), so the feature trace should surface that distinction instead of presenting the archive SHA-256 as the add-on identity. Retaining unavailable status keeps evidence-linked classification honest.

<!-- gh-comment-id:6186376003 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed: an unavailable native add-on must not inherit its containing archive digest — the analysis already knows the add-on bytes are unavailable (hash_status unavailable, inventory_artifact_id null), so the feature trace should surface that distinction instead of presenting the archive SHA-256 as the add-on identity. Retaining unavailable status keeps evidence-linked classification honest.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1534
No description provided.