[GH-ISSUE #4099] [Feature] Make the Windows native artifact build bit-for-bit reproducible #1615

Open
opened 2026-10-16 21:08:01 +00:00 by bsakaguchi · 0 comments
Owner

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

Area: Setup, packaging, or upgrade
Expected interaction with targets: No target interaction (packaging only)
Verification available so far: I have a prototype or observed results

Filed with gh, so the feature-request form fields are inlined above. I searched open and closed issues and PRs for reproducible build, no-insert-timestamp and dlltool and found no related proposal. No credentials, private samples or sensitive target data below.

Current limitation

native/windows/build/rea-windows-x64.node is the one prebuilt binary REA ships, and it holds the most privilege in the package: Job Objects, DACLs, token handling and NtCreateFile-based path admission. The source is public and the loader verifies the addon's SHA-256 against manifest.json before granting authority, but that check confirms the artifact matches its own manifest — it does not let anyone confirm the shipped bytes came from the published source.

I tried to close that gap and found the build lane is already very close to deterministic: -Wl,--no-insert-timestamp is set, dependencies are pinned, and the Node-API headers are SHA-256 checked. Two remaining variables prevent an independent rebuild from matching:

  1. The import library path leaks the builder's $HOME into the binary. scripts/build-windows-native.mjs generates the delay-import library at homedir()/.cache/rea-windows-native/libnode-api.a, and dlltool embeds that absolute path into six symbol names. The published 6.5.0 artifact therefore contains __IAT__home_runner__cache_rea_windows_native_libnode_api_a and five siblings. Any builder whose $HOME is not /home/runner produces a different — and differently sized — binary.

  2. ImageBase is not fixed. GNU ld picks a non-deterministic image base for the DLL. The published 6.5.0 artifact uses 0x335910000; my rebuilds landed elsewhere. This changes no code, but it shifts every absolute pointer, so the artifact digest differs on every build.

Neither is a correctness problem. Both are small, and fixing them would make the one binary users cannot read verifiable by anyone.

Use cases

Users evaluating REA before granting it filesystem and process authority. The source is auditable, and I confirmed by static analysis that the addon imports nothing from ws2_32/wininet/winhttp — only ADVAPI32, BCRYPT, KERNEL32, MSVCRT and NTDLL — but static review of the source cannot establish that the shipped binary was built from it.

This also matters for the project's own supply-chain posture: a reproducible artifact lets a second builder detect a compromised release runner, which a self-referential manifest cannot.

Desired outcome and success criteria

npm run build:windows-native, run on any host with the pinned toolchain, produces a byte-identical rea-windows-x64.node for a given tag — independent of the builder's $HOME, username, or working directory.

Success criteria:

  • Two builds of the same tag on different hosts yield the same SHA-256.
  • The expected digest per released version is published (release notes, or a committed file), so a user can compare without reverse-engineering the build environment.
  • Ideally verify:windows-native:artifact gains a mode that compares a freshly built artifact against the published digest.

Verification details

I reproduced the published 6.5.0 artifact bit for bit after neutralising exactly the two variables above, which is what makes me confident they are the only ones.

Environment: ubuntu:24.04 container, g++-mingw-w64-x86-64 from apt (x86_64-w64-mingw32-g++ (GCC) 13-win32), Node 24.18.0, source from tag rea-agents-6.5.0, unmodified scripts/build-windows-native.mjs except where noted.

Build Change Result
1 CI recipe as-is ($HOME=/root) 873551 bytes, 20c5f948… — 42 bytes smaller, 18.2% of bytes differ
2 HOME=/home/runner 873593 bytes, 1e569314… — exact size match, identical string set (6120/6120, zero differences either way), 0.56% of bytes differ
3 + -Wl,--image-base,0x335910000 665c57ab90aef0712a6aafc423c7992a88bf608bea63b370cb30c695900f9aed — identical to the published artifact; cmp reports 0 differing bytes

Build 2 is the informative one: with only $HOME aligned, size and the full string inventory already matched exactly, and the residual 0.56% was entirely absolute pointers shifted by the differing image base. The 42-byte delta in build 1 is also fully accounted for: home_runner (11 chars) minus root (4) = 7 bytes across 6 embedded symbol names.

Possible direction

Both look like one-line changes in scripts/build-windows-native.mjs:

  1. Generate or link the import library through a path that does not depend on $HOME — e.g. keep the download cache where it is, but place libnode-api.a at a fixed repo-relative path (.cache/node-api/libnode-api.a) and pass it relative to root, which is already the cwd for run(). The embedded symbol names then become build-host independent.
  2. Add an explicit -Wl,--image-base,<constant> next to the existing -Wl,--no-insert-timestamp. Reusing the current 0x335910000 would keep 6.5.0's digest stable.

I have not opened a PR because the image-base constant is a project decision (any fixed, suitably aligned high value works) and the cache layout change touches the --inspection-fixture path too. Happy to send one if the direction is agreeable.

Alternatives considered

  • Trusting manifest.json. It is self-referential: a tampered artifact would ship a matching manifest.
  • Static analysis of the binary alone. Useful, and I did it — the import table and string inventory are consistent with the source — but it establishes what the binary does, not that it came from the published source.
  • Building locally and using that artifact instead. The README documents this, but it does not verify the published binary, and the loader checks the manifest version and digest, so a self-built artifact needs its own manifest.

References

  • scripts/build-windows-native.mjs — cache path, dlltool invocation, link flags
  • native/windows/README.md — "Build" and "Restore the artifact in a source checkout"
  • .github/workflows/ci.yml — the ubuntu-latest + g++-mingw-w64-x86-64 lane
Originally created by @soybabydev on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4099 **Area:** Setup, packaging, or upgrade **Expected interaction with targets:** No target interaction (packaging only) **Verification available so far:** I have a prototype or observed results <sub>Filed with `gh`, so the feature-request form fields are inlined above. I searched open and closed issues and PRs for `reproducible build`, `no-insert-timestamp` and `dlltool` and found no related proposal. No credentials, private samples or sensitive target data below.</sub> ## Current limitation `native/windows/build/rea-windows-x64.node` is the one prebuilt binary REA ships, and it holds the most privilege in the package: Job Objects, DACLs, token handling and `NtCreateFile`-based path admission. The source is public and the loader verifies the addon's SHA-256 against `manifest.json` before granting authority, but that check confirms the artifact matches *its own* manifest — it does not let anyone confirm the shipped bytes came from the published source. I tried to close that gap and found the build lane is already very close to deterministic: `-Wl,--no-insert-timestamp` is set, dependencies are pinned, and the Node-API headers are SHA-256 checked. Two remaining variables prevent an independent rebuild from matching: 1. **The import library path leaks the builder's `$HOME` into the binary.** `scripts/build-windows-native.mjs` generates the delay-import library at `homedir()/.cache/rea-windows-native/libnode-api.a`, and `dlltool` embeds that absolute path into six symbol names. The published 6.5.0 artifact therefore contains `__IAT__home_runner__cache_rea_windows_native_libnode_api_a` and five siblings. Any builder whose `$HOME` is not `/home/runner` produces a different — and differently sized — binary. 2. **`ImageBase` is not fixed.** GNU ld picks a non-deterministic image base for the DLL. The published 6.5.0 artifact uses `0x335910000`; my rebuilds landed elsewhere. This changes no code, but it shifts every absolute pointer, so the artifact digest differs on every build. Neither is a correctness problem. Both are small, and fixing them would make the one binary users cannot read verifiable by anyone. ## Use cases Users evaluating REA before granting it filesystem and process authority. The source is auditable, and I confirmed by static analysis that the addon imports nothing from `ws2_32`/`wininet`/`winhttp` — only ADVAPI32, BCRYPT, KERNEL32, MSVCRT and NTDLL — but static review of the *source* cannot establish that the shipped *binary* was built from it. This also matters for the project's own supply-chain posture: a reproducible artifact lets a second builder detect a compromised release runner, which a self-referential manifest cannot. ## Desired outcome and success criteria `npm run build:windows-native`, run on any host with the pinned toolchain, produces a byte-identical `rea-windows-x64.node` for a given tag — independent of the builder's `$HOME`, username, or working directory. Success criteria: - Two builds of the same tag on different hosts yield the same SHA-256. - The expected digest per released version is published (release notes, or a committed file), so a user can compare without reverse-engineering the build environment. - Ideally `verify:windows-native:artifact` gains a mode that compares a freshly built artifact against the published digest. ## Verification details I reproduced the published 6.5.0 artifact **bit for bit** after neutralising exactly the two variables above, which is what makes me confident they are the only ones. Environment: `ubuntu:24.04` container, `g++-mingw-w64-x86-64` from apt (`x86_64-w64-mingw32-g++ (GCC) 13-win32`), Node 24.18.0, source from tag `rea-agents-6.5.0`, unmodified `scripts/build-windows-native.mjs` except where noted. | Build | Change | Result | |---|---|---| | 1 | CI recipe as-is (`$HOME=/root`) | 873551 bytes, `20c5f948…` — 42 bytes smaller, 18.2% of bytes differ | | 2 | `HOME=/home/runner` | 873593 bytes, `1e569314…` — **exact size match, identical string set (6120/6120, zero differences either way)**, 0.56% of bytes differ | | 3 | + `-Wl,--image-base,0x335910000` | `665c57ab90aef0712a6aafc423c7992a88bf608bea63b370cb30c695900f9aed` — **identical to the published artifact; `cmp` reports 0 differing bytes** | Build 2 is the informative one: with only `$HOME` aligned, size and the full string inventory already matched exactly, and the residual 0.56% was entirely absolute pointers shifted by the differing image base. The 42-byte delta in build 1 is also fully accounted for: `home_runner` (11 chars) minus `root` (4) = 7 bytes across 6 embedded symbol names. ## Possible direction Both look like one-line changes in `scripts/build-windows-native.mjs`: 1. Generate or link the import library through a path that does not depend on `$HOME` — e.g. keep the download cache where it is, but place `libnode-api.a` at a fixed repo-relative path (`.cache/node-api/libnode-api.a`) and pass it relative to `root`, which is already the `cwd` for `run()`. The embedded symbol names then become build-host independent. 2. Add an explicit `-Wl,--image-base,<constant>` next to the existing `-Wl,--no-insert-timestamp`. Reusing the current `0x335910000` would keep 6.5.0's digest stable. I have not opened a PR because the image-base constant is a project decision (any fixed, suitably aligned high value works) and the cache layout change touches the `--inspection-fixture` path too. Happy to send one if the direction is agreeable. ## Alternatives considered - **Trusting `manifest.json`.** It is self-referential: a tampered artifact would ship a matching manifest. - **Static analysis of the binary alone.** Useful, and I did it — the import table and string inventory are consistent with the source — but it establishes what the binary does, not that it came from the published source. - **Building locally and using that artifact instead.** The README documents this, but it does not verify the published binary, and the loader checks the manifest version and digest, so a self-built artifact needs its own manifest. ## References - `scripts/build-windows-native.mjs` — cache path, `dlltool` invocation, link flags - `native/windows/README.md` — "Build" and "Restore the artifact in a source checkout" - `.github/workflows/ci.yml` — the `ubuntu-latest` + `g++-mingw-w64-x86-64` lane
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#1615
No description provided.