Skip to content

Stop Intel Mac before release lookup and pin the verified head CID - #398

Draft
SashaMIT wants to merge 17 commits into
cursor/intel-package-name-1297from
cursor/intel-install-gate-1297
Draft

SashaMIT wants to merge 17 commits into
cursor/intel-package-name-1297from
cursor/intel-install-gate-1297

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stack: #395 ← #397 (cursor/intel-package-name-1297, this PR's base) ← #398 (this) ← #399.

Head: da63c235. Draft. Do not merge. The base (#397, 8dd9e851) is merged in (29733e73, merge, not rebase); it carries the zip 9 fix from #417 so cargo-audit can pass, #394's 36720070 (/healthz and the gateway start prove that the gateway holds the lock from the held flock; docs/LAUNCHER_CONTRACT.md, "What proves that this gateway holds the lock") and #395's macOS smoke test fixes through 3cffaad4. 32f4abfd merges #397 at 56a8084f (no conflicts), which carries #394's 4bb19dcf: the ownership proof is one step (each registry entry keeps a second descriptor of the locked open file, and the registry mutex is held from the lookup to the end of the probe; 85f3142e); the managed-runtime subordinate lane uses the same proof as /healthz, and a Home terminal gets an inherited lock descriptor, ELASTOS_GATEWAY_HOST_LOCK_FD (eed83a72); a held lock whose record cannot be read still counts as held (4bb19dcf). 9c7ebae6 merges #397 at aa7bd1ce (no conflicts; docs/LAUNCHER_CONTRACT.md and scripts/gateway-healthz-smoke-test.py auto-merged), which carries #394's 13cdcad0 (the contract states that the host lock proves possession, not that the holder is the gateway) and #395's 630efc6d (the default-command smoke test no longer depends on port 8090). The base merges do not change this PR's diff against #397.

Round 4 commits on top of 29733e73:

  • f8defe86 test only: the swapped-Runtime test now swaps the real download on macOS too (see "Why the swap test failed on a Mac").
  • 112e168f install.sh: the staged Runtime runs from one held file, not by path (item 5 below).

Round 5 commit:

  • da63c235 test only: the Linux case of the swapped-Runtime test no longer uses the host's mktemp (see "Round 5: the Linux case assumed GNU mktemp").

What this PR does in scripts/install.sh:

  1. Intel Mac stop. detect_platform stops an Intel Mac before any download and exits 1 with exactly Intel Mac is not supported yet. The text lives once, in INTEL_MAC_UNSUPPORTED (install.sh:327); the stop (:357) and --help (:113) print that variable. docs/MAC.md#intel-mac names the variable and quotes it.
  2. Rosetta. A Terminal under Rosetta on an Apple silicon Mac reports uname -m x86_64. On Darwin x86_64, mac_has_apple_silicon (install.sh:332) asks the kernel: sysctl -in sysctl.proc_translated or sysctl -in hw.optional.arm64 must print 1. Then the platform is aarch64-darwin. That build is arm64 only (package-release-binaries.sh refuses a universal binary), so macOS runs it natively when the Rosetta shell starts it. Any other answer, a failed sysctl or a missing sysctl proves nothing, so the installer stops with the Intel Mac message before any download (fail closed).
  3. Verified head CID. A non-empty HEAD_CID stamp must equal the UnixFS CIDv0 of the verified release-head.json bytes, or the run exits 1. An empty stamp stays empty, so a frozen installer does not cache a head CID (8e3c774f).
  4. Use only checked envelope bytes.
    • verify_signature (install.sh:760) reads the envelope once and returns the SHA-256 of the bytes whose signature passed (HEAD_SHA256 :1427, RELEASE_SHA256 :1459).
    • Every later read (bind_verified_head_cid, json_get, validate_release_identity) takes that SHA-256 and stops with RELEASE_CHANGED (:161) for other bytes.
    • sources.json pins MAINTAINER_DID (:1557), the trust root that verify_signature compared with the signer. Before, it read d["signer_did"] from the file again.
    • The writer (update.rs install_release) reads each file once, checks those bytes against the pin, version, channel and any stamp in sources.json, and installs those same bytes. It admits the candidate Runtime only if the bytes it read have the signed SHA-256 (installed_release.rs admit_candidate).
  5. Run the staged Runtime from one held file (112e168f). Before, the installer hashed the staged copy by path, then ran it by path three times (--version, install-release --check, install-release). The staged Runtime is the installer's writer, so a process that replaced or rewrote that path between the hash and a run got its own program run as the writer. Now:
    • The copy sits read-only (0500) in a new 0700 folder made by mktemp -d inside the checked install directory (:1518).
    • It is opened once on fd 9 (:1526). held_runtime hash (:408) takes its SHA-256 from that open file and records its identity (device, inode, size, mtime, ctime).
    • Before each run, held_runtime check (:1540, :1622, :1635) refuses with RUNTIME_CHANGED (:159) unless the held file is unchanged, is a regular file of this user with mode 0500 and one link, and the path still names it.
    • Where the kernel can start an open file (Linux, /proc/self/fd/9, :1533), run_held_runtime (:449) starts the held file itself, so a path swapped after the last check cannot run. macOS has no way to start an open file (no fexecve), so there the path runs right after the check. fd 9 is closed after the install (:1638).

Why the swap test failed on a Mac

The round-3 hook swapped a file only when its path was under TMPDIR. Apple's mktemp -d (no template) ignores TMPDIR: in its implied -t mode it uses the per-user folder from confstr(_CS_DARWIN_USER_TEMP_DIR) (apple-oss-distributions/shell_cmds mktemp/mktemp.c, the #ifdef __APPLE__ branch in if (tflag)). GNU mktemp -d uses TMPDIR. So on a Mac nothing was swapped, the install succeeded, and the test failed on returncode. Reproduced on Linux with a mock that behaves like Apple's mktemp -d: the round-3 hook gives exit 0 and the original Runtime installed.

Now the hook swaps the exact path curl wrote (the curl mock logs it), once, and logs the swap (hasher, path, SHA-256 before and after). The test asserts the swap happened before it asserts the refusal, so it cannot pass or fail without a swap. sha256sum and shasum -a 256 are both mocked. TMPDIR is a private 0700 folder the test made.

Round 5: the Linux case assumed GNU mktemp

Review (round 4): "the swapped-Runtime test's Linux subcase assumes GNU mktemp, so it fails on every Mac host (install-bootstrap-test.py:1112)". Confirmed. The Darwin cases mock Apple's mktemp (mock_apple_mktemp), but the Linux case used the host's own mktemp and then required the scratch folder to be under the fixture TMPDIR (:1111-1112). On a Mac host that mktemp is Apple's, which ignores TMPDIR.

da63c235 adds mock_gnu_mktemp (install-bootstrap-test.py:826): mktemp -d with no template makes ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX; every other form goes to the host mktemp unchanged. It uses only a template ending in X, which GNU and BSD mktemp both accept. The Linux case uses it (:1117), so each of the three cases now mocks mktemp and none depends on the host.

Proof without a Mac: a stub mktemp first on PATH that behaves like Apple's (only -d -q -t -u; --tmpdir and -p refused with illegal option; no template and no -t uses a per-user folder, not TMPDIR; a template must end in X).

  • Before (9c7ebae6), with the stub: the Linux case fails at the reviewer's line:
    File "scripts/install-bootstrap-test.py", line 1112, in test_runtime_swapped_after_its_checksum_check_is_not_run_or_installed
    AssertionError: PosixPath('/tmp/.../bsdmk/user-temp') != PosixPath('/home/user/wt/rt-398/target/installer-sand...tmp')
    FAILED (failures=1)
    
    The whole file with the stub at 32f4abfd: Ran 98 tests ... FAILED (failures=1, skipped=4), and that one subcase is the only failure, so no other test depends on GNU mktemp.
  • After (da63c235): that test OK with the stub and with the host's GNU mktemp; the whole file Ran 98 tests ... OK (skipped=4) both ways.

No real Mac ran this. No CI job runs install-bootstrap-test.py on macOS: it runs only in source-gate (ubuntu-24.04) through scripts/test-source-gate.sh release → release-platform-input-test.py (InstallerAdmissionTest). The macOS jobs (gateway-smoke-macos-intel, source-home-macos) do not run it. So CI on this head proves the Linux path only.

Decision: Intel Mac message

Decision 4: the plain stop Intel Mac is not supported yet., with no pointer to the developer source-home build and no sentence before it. The launcher (Elacity/elastos-launcher) has its own Intel stop text (src/main/targets/runtimeSupport.ts:4-5); this PR does not change the launcher.

Tests

On 112e168f (Linux):

$ python3 scripts/install-bootstrap-test.py
test_held_runtime_refuses_any_change_to_the_held_file_or_its_path ... ok
test_runtime_swapped_after_its_checksum_check_is_not_run_or_installed ... ok
test_staged_runtime_swapped_between_its_hash_and_its_run_is_refused ... ok
test_staged_runtime_swapped_after_its_last_check_still_runs_the_checked_bytes ... ok
test_release_head_swapped_after_its_signature_check_is_refused ... ok
test_publisher_pin_is_the_trust_root_not_a_second_read ... ok
Ran 98 tests in 70.603s
OK (skipped=4)

$ python3 scripts/install-bootstrap-test.py --bash <bash 3.2.57 build>
Ran 98 tests in 68.766s
OK (skipped=4)

$ python3 scripts/gateway-healthz-smoke-test.py
Ran 26 tests in 48.737s
OK (skipped=3)

git diff --check clean. A real elastos ELF (debug build) starts through /proc/self/fd/9 with exec -a and prints its version.

On 32f4abfd (the base merge): install-bootstrap-test.py OK (skipped=4); gateway-healthz-smoke-test.py OK (skipped=3).

On da63c235: install-bootstrap-test.py 98 OK (skipped=4) with the host mktemp and with the Apple-like stub; release-platform-input-test.py 164 OK and release-signer-test.py 87 OK (both under env -i PATH=... HOME=... python3 -I -S; in a shell with TLS proxy variables the signer refuses them, as #397 notes); gateway-healthz-smoke-test.py 26 OK (3 skipped); ci-release-policy-test.py 103 OK; package-release-binaries-test.py 9 OK; git diff --check, public-copy-entropy-check pass.

The tamper tests:

  • test_runtime_swapped_after_its_checksum_check_is_not_run_or_installed: the download is rewritten right after its SHA-256 passed. Three cases: Linux with sha256sum and a GNU-like mktemp mock; Darwin (uname mock, arm64) with Apple's mktemp -d layout and TMPDIR behind a symlink (like /tmp → /private/tmp); the same with no sha256sum on PATH, so shasum is used.
  • test_staged_runtime_swapped_between_its_hash_and_its_run_is_refused: the staged Runtime is replaced at its path, or rewritten in place with its mode put back, right after held_runtime hash. Linux and Darwin layouts. Refused before anything runs.
  • test_staged_runtime_swapped_after_its_last_check_still_runs_the_checked_bytes (Linux only, skipped where /proc/self/fd is missing): the path is replaced after the last check. The checked bytes run, the replacement never runs, and the writer refuses the replaced candidate, as Runtime's writer does.
  • test_held_runtime_refuses_any_change_to_the_held_file_or_its_path: path renamed over, removed, made a symlink, file written in place, made writable, given a second link.

Negative checks (the lines reverted, then restored):

  • held_runtime check made a no-op: the staged-swap test fails 4 of 4 (AssertionError: True is not false on swapped-ran: the swapped Runtime ran), and the unit test fails 6 of 6 (0 != 12).
  • HELD_RUNTIME=/proc/self/fd/9 removed (path exec, as on macOS): the after-the-last-check test fails (True is not false: the swapped Runtime ran). This is the macOS window that remains.
  • Round-3 install.sh (29733e73, hash by path then run by path): the staged-swap test fails 4 of 4 (True is not false).
  • Round-3 staged re-hash removed (29733e73 install.sh:1469-1471) with the new download-swap test: fails 3 of 3 (True is not false), after the swap assertions passed.
  • Earlier rounds (unchanged): the three sys.exit(3) SHA-256 comparisons removed → test_release_head_swapped_after_its_signature_check_is_refused exits 0; pin set back to d["signer_did"] → test_publisher_pin_is_the_trust_root_not_a_second_read fails.

CI

  • This PR is stacked on Name the Intel Mac release package darwin-amd64 #397, so its CI tests it merged into Name the Intel Mac release package darwin-amd64 #397's branch, not develop. develop's own Home link test failure (403 vs 200, fixed in develop by bfefe92) did not apply to this PR.
  • For 29733e73 (run 39038170012): source-gate, cargo-audit, release-upstream-paths and gateway-smoke-macos-intel passed; the other jobs were still queued when 112e168f was pushed. No job failed. CI runs install-bootstrap-test.py on Linux only (source-gate through release-platform-input-test.py).

Principles check

  • Fail closed: an x86_64 Mac without the kernel's 1 stops before any download. A staged Runtime that changed after its check does not run.
  • Signed trust chain: every read after a check uses the checked bytes; the pin is the trust root; the program that runs as the writer is the held, hashed file.
  • Plain language: the Intel stop is one sentence. The change message says what happened and the next step.

Gaps, not fixed here:

  • Another process of the same user can always win: it can rewrite the held file in place after the last check (Linux), swap the path between the last check and exec (macOS), or edit the installed Runtime later. No installer can stop a process with the user's own rights. Other users cannot write the staged file or its 0700 folder.
  • The download folder is still mktemp -d (TMPDIR on Linux, the per-user folder on macOS). A swap there is refused, not prevented.
  • The install directory's parent folders are not checked (the data directory's are). A parent swapped before a run is refused by the path check; after the last check, Linux still runs the held file and macOS has the window above.
  • The maintainer DID can be overridden by env or flag (install.sh:45, :1265-1267). The team decided this in Follow-ups from Sasha's 0.8.0 pre-release review (#375) #434 for 0.8.1; Stamped installer: no environment or flag override of the maintainer DID #435 (stacked on this PR) makes the stamped installer refuse a different DID. Unchanged here.
  • Not run on a real Mac. The Darwin cases fake uname, Apple's mktemp -d layout and a PATH without sha256sum on Linux, and the round-5 check used an Apple-like mktemp stub on Linux. The path-exec branch that macOS takes was run on Linux only by forcing it (the second revert check): the staged-swap test passes on it, the after-the-last-check test fails as expected. Not tested on a real Rosetta Terminal or a real Intel Mac. No CI job runs this suite on macOS.

No grade here is a merge approval.

Team issues to open

  1. Signed Intel artifact and journey. Title: Signed x86_64-darwin artifact, darwin-amd64 payloads, and Home journey.
  2. Adversarial review of the head CID and the check-then-use binding. An independent review on the exact SHA.
  3. Maintainer DID override (asked on this PR; decided in Follow-ups from Sasha's 0.8.0 pre-release review (#375) #434, implemented in Stamped installer: no environment or flag override of the maintainer DID #435).
  4. Launcher wording. Optional: make the launcher's Intel stop use the same words as docs/MAC.md#intel-mac.
  5. Installer tests on macOS CI. Optional: run scripts/install-bootstrap-test.py in a macOS job, so a host-specific assumption like this one is caught by CI.

@cursor
cursor Bot force-pushed the cursor/intel-package-name-1297 branch from 4344c8d to 1f043f3 Compare October 16, 2026 05:37
@cursor
cursor Bot force-pushed the cursor/intel-install-gate-1297 branch from f401ae9 to 355bddd Compare October 16, 2026 05:37
@cursor
cursor Bot force-pushed the cursor/intel-package-name-1297 branch from decf029 to c68f402 Compare October 16, 2026 11:54
@cursor
cursor Bot force-pushed the cursor/intel-install-gate-1297 branch from 02c09d0 to 3e94f2f Compare October 16, 2026 11:57
@cursor cursor Bot changed the title fix: let the installer select x86_64-darwin on Intel Mac Stop Intel Mac before release lookup and pin the verified head CID Oct 16, 2026
cursoragent and others added 2 commits October 16, 2026 12:20
The installer hashes the signed release-head.json it just checked and
writes that UnixFS CIDv0 into sources.json. A non-empty stamp that names
different bytes exits 1. The launcher contract records that failure.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
detect_platform exits before any download when the Mac is not Apple
silicon. The message names Intel Mac and the source-home path in
docs/MAC.md. Help text states that there is no signed Intel download.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/intel-package-name-1297 branch from c68f402 to 3e52e38 Compare October 16, 2026 12:20
@cursor
cursor Bot force-pushed the cursor/intel-install-gate-1297 branch from 3e94f2f to 58a76ac Compare October 16, 2026 12:20
A frozen installer has no head CID. Filling that stamp from the verified
bytes made a fresh install cache a head CID, which the update hop refuses.
A matching stamp still stores the verified CID. A different stamp still stops.

Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
docs/MAC.md: kept the base's one Intel Mac section and quoted this branch's new installer message in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
install.sh now stops an Intel Mac with "Intel Mac has no signed release yet.
See docs/MAC.md for the source-home path." The test requires that stop in
detect_platform, requires detect_platform before release-head.json in the
main flow, requires MAC.md to quote the same text, and refuses the old text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
zip 9 for RUSTSEC-2026-0367/0368/0369 so cargo-audit passes. The base
already merges fix/zip-9-rustsec; merging the base keeps this PR's diff
against it unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
The installer stop on an Intel Mac pointed to the source-home path in
docs/MAC.md. That path is a developer build, not an install path, so a
first-time user had no step they could take. The team decided on a plain
stop with no pointer to that build.

- install.sh detect_platform now stops with "This Mac has an Intel
  processor. ElastOS supports Macs with Apple silicon (M1 or later). Intel
  Mac is not supported yet." It still exits 1 before any download.
- --help says "Intel Mac is not supported yet." instead of the
  source-home pointer.
- docs/MAC.md#intel-mac quotes the new message and says there is no other
  install path for an Intel Mac.
- gateway-healthz-smoke-test.py requires the new text in install.sh and
  MAC.md, requires detect_platform before the first fetch_url, and refuses
  the retired texts.
- install-bootstrap-test.py: test_intel_mac_stops_before_release_lookup
  requires exit 1, no PLATFORM output and no "source-home" in the message.
  New test_intel_mac_full_run_downloads_nothing runs the whole install.sh
  with a configured publisher, uname reporting Darwin x86_64 and curl/wget
  mocks that log. It requires no network call, no file in HOME, exit 1 and
  the message. With the die line removed, it records the curl fetch of
  release-head.json and fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings the plain Intel Mac stop message from #398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Brings in the ported held-lock journal test fix (develop 5051505) so test-capsules stops flaking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Decision 4 is the plain stop. The installer put two sentences about the
processor and Apple silicon before it.

- install.sh keeps the text once, in INTEL_MAC_UNSUPPORTED. detect_platform
  and --help both print that variable.
- docs/MAC.md#intel-mac names the variable and quotes its text.
- gateway-healthz-smoke-test.py requires the variable, the die line, a
  single copy of the text, and none of the retired sentences.
- install-bootstrap-test.py requires stderr to be exactly
  "Error: Intel Mac is not supported yet." from detect_platform, and from a
  full run that downloads nothing and writes nothing in HOME.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
A Terminal under Rosetta on an M1 or later Mac reports uname -m x86_64,
so the installer told an Apple silicon Mac that it was an Intel Mac.

On Darwin x86_64, detect_platform now asks the kernel. When
sysctl -in sysctl.proc_translated or sysctl -in hw.optional.arm64 prints 1,
the platform is aarch64-darwin. That build is arm64 only, so macOS runs it
natively when the Rosetta shell starts it. Any other answer, a failed
sysctl or a missing sysctl proves nothing, so the installer stops with the
Intel Mac message before any download. Linux never asks sysctl.

Tests (install-bootstrap-test.py, sysctl and uname faked):
- test_rosetta_shell_on_apple_silicon_is_apple_silicon: four Rosetta or
  arm64 answers give aarch64-darwin; Linux x86_64 stays x86_64-linux.
- test_intel_mac_stops_before_release_lookup: 0/0, empty, 0/empty, 2/0,
  a failing sysctl and a missing sysctl all stop with exit 1.
- test_intel_mac_full_run_downloads_nothing: the whole installer with an
  Intel answer, a failing sysctl, and no sysctl on PATH runs no curl and
  writes nothing in HOME.
- test_rosetta_shell_installs_the_apple_silicon_build_and_intel_downloads_nothing:
  one sandbox; the Intel answer makes no request, then the Rosetta answer
  fetches artifacts/elastos-aarch64-darwin and installs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
The installer checked release-head.json, release.json and the Runtime,
then read or copied them again from the temporary folder. Nothing tied
those later reads to the bytes that passed. A process that could change
that folder between the check and the use could:
- put a head signed by its own key in place, and sources.json pinned
  that key as the publisher, read again from the file (d["signer_did"]);
- put another Runtime in place after its SHA-256 passed. The installer
  ran that copy (--version, install-release) and installed it.

Now:
- verify_signature reads the envelope once and returns the SHA-256 of
  the bytes whose signature passed (VERIFIED_SHA256).
- Every later read (bind_verified_head_cid, json_get,
  validate_release_identity) takes that SHA-256 and stops with
  "The release files changed on this computer after they were checked.
  Nothing was installed. Run the installer again." for other bytes.
- sources.json pins MAINTAINER_DID, the trust root that
  verify_signature compared with the signer. It is not read again.
- The staged Runtime copy in the install directory is checked again
  after the copy. That copy is the file the installer runs and hands to
  the writer. The writer reads each file once, checks those bytes against
  sources.json and installs those same bytes (update.rs install_release).

Tests (install-bootstrap-test.py):
- test_runtime_swapped_after_its_checksum_check_is_not_run_or_installed:
  a sha256sum hook swaps the download in TMPDIR after it hashes it. The
  swapped Runtime never runs and nothing is installed. Without the staged
  check, the swapped Runtime runs and is installed.
- test_release_head_swapped_after_its_signature_check_is_refused: a python3
  hook swaps in an attacker-signed head after its signature passes. The run
  stops after one request. Without the SHA-256 checks it installs.
- test_publisher_pin_is_the_trust_root_not_a_second_read: the head is
  swapped after the last read; sources.json still pins the maintainer DID.
  With the old d["signer_did"] read, it pins the attacker key.
- test_reads_after_the_signature_check_refuse_other_bytes: each read
  refuses one changed byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

@andersalm @SashaMIT a team question on the installer trust root (signing keys, so I am not changing it without your answer).

Issue. scripts/install.sh:45 reads MAINTAINER_DID="${ELASTOS_MAINTAINER_DID:-__MAINTAINER_DID__}", and --maintainer-did (install.sh:1213-1215) replaces it too. The env var wins even over the DID the release signer stamps into the published installer. Whoever controls the environment or the command line of the install (a copied "install" command with ELASTOS_MAINTAINER_DID=... bash, a wrapper script, a hostile launcher or shell profile) replaces the trust root. The installer then accepts a release signed by that key and pins it in sources.json as the publisher, so later elastos update runs trust it too.

Who uses the override today. Only repo test and demo paths: scripts/install-bootstrap-test.py (passes --maintainer-did), home-frontdoor-smoke.sh, home-demo-local.sh, local-carrier-chat-smoke.sh, local-identity-profile-smoke.sh (all set ELASTOS_MAINTAINER_DID). update-hop-compare.py already refuses a frozen installer run when the env var is set. A fork with its own key would stamp its own installer through the signer, so it would not need the override.

Options.

  • (a) Remove the override. A stamped installer uses only its stamp; an unstamped one (repo checkout) stops. Tests and smokes stamp a copy of the script, as install-bootstrap-test.py already does for the source ticket.
  • (b) Allow it only in an unstamped installer (the placeholder is still there), or only with an explicit flag such as --untrusted-maintainer-did, which prints a loud warning and never pins that key in sources.json.
  • (c) Keep it as it is.

Recommendation: (a). The published installer has exactly one trust root, and test paths stamp a copy. (b) with "unstamped only" is the fallback if a smoke really needs the env var.

No code changes on this until you answer.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

51404a76: Intel stop is now exactly "Intel Mac is not supported yet." (one variable); a Rosetta Terminal on Apple silicon (sysctl.proc_translated/hw.optional.arm64 = 1) installs the aarch64-darwin build, anything else stops before any download.
Every read after a check now must see the checked SHA-256, the pin is MAINTAINER_DID (not re-read from the file), and the staged Runtime is hashed again before it runs.
Proof: install-bootstrap-test.py test_rosetta_shell_*, test_intel_mac_*, test_runtime_swapped_after_its_checksum_check_is_not_run_or_installed, test_release_head_swapped_after_its_signature_check_is_refused, test_publisher_pin_is_the_trust_root_not_a_second_read (all fail on b60b6f0a).


Generated by Claude Code

SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings in #398's fixes: the plain Intel Mac stop, Rosetta as Apple
silicon, and the installer using only the bytes it checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Brings in #394's flock ownership proof (3672007) and #395's macOS smoke test fixes (3cffaad), through #397.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings in #394's flock ownership proof (3672007) and #395's macOS smoke test fixes, through #397 and #398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 3 review: blocked (C). Removing the maintainer-key override is a team decision.
The macOS swapped-Runtime negative test fails locally; make its temp folder explicit and prove the swap.
Hardening: run the staged installer from one held file, not by path twice.

The round-3 hook swapped a file only when its path was under TMPDIR. Apple's
mktemp -d ignores TMPDIR and uses the per-user folder from confstr, so on a
Mac nothing was swapped, the install succeeded and the test failed.

The hook now swaps the exact path curl wrote, logs the swap, and the test
asserts the swap happened before it asserts the refusal. sha256sum and
shasum are both mocked. Extra cases run as Darwin with Apple's mktemp
layout, a symlinked TMPDIR, and a PATH without sha256sum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
@andersalm

Copy link
Copy Markdown
Contributor

Answer to the trust-root question (comment 6233893115): (a), in 0.8.1, tracked in #434. Remove the ELASTOS_MAINTAINER_DID environment and --maintainer-did override from the stamped installer. The out-of-band check stays as documented: compare the installer's printed Maintainer DID: with the DID in the README. 0.8.0 ships the stamped installer unchanged; the signed release.json binds its bytes. Irzhy has the security sign-off on the change itself.

The installer hashed the staged Runtime by path, then ran it by path three
times. A process that replaced or rewrote that path between the hash and
a run got its own program run as the installer's writer.

The staged copy now sits read-only (0500) in a new 0700 folder made by
mktemp -d in the checked install directory, and is opened once on fd 9.
Its SHA-256 is taken from that open file. Before each run, a check refuses
unless the held file is unchanged (device, inode, size, mtime, ctime, mode
0500, one link, this user) and the path still names it. Where the kernel
can start an open file (Linux /proc/self/fd), the held file itself runs,
so a path swapped after the last check cannot run. macOS has no way to
start an open file, so there the path runs right after the check.

Tests: staged swap (rename and in place) between the hash and the run is
refused on the Linux and Darwin layouts; a swap after the last check runs
only the checked bytes (Linux); held_runtime refuses each kind of change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

f8defe86: the Mac failure was Apple's mktemp -d ignoring TMPDIR, so the old hook never swapped. The hook now swaps the exact path curl wrote and the test asserts the swap first (Darwin layout, shasum, symlinked TMPDIR cases).
112e168f: the staged Runtime is held open on fd 9, hashed from that fd, checked before each run, and on Linux started from /proc/self/fd/9. Proof: test_staged_runtime_swapped_between_its_hash_and_its_run_is_refused and ..._after_its_last_check_still_runs_the_checked_bytes fail on 29733e73.
The maintainer DID override is unchanged; it is still waiting on the team.


Generated by Claude Code

SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings in #398's round-4 commits: the swapped-Runtime test swaps the real
download on macOS too (f8defe8), and the staged Runtime runs from one
held file, not by path (112e168).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

Trust-root decision (#434) is now draft #435, stacked on this PR, for 0.8.1. A stamped installer refuses a different DID from ELASTOS_MAINTAINER_DID or --maintainer-did before any download. The same DID is accepted, because the launcher passes it.
Proof: install-bootstrap-test.py test_rendered_installer_refuses_another_did_from_environment_or_flag_before_any_download runs on the real installer as render_installer renders it, and it fails when the guard is reverted. Needs Irzhy's sign-off.


Generated by Claude Code

Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc), through #395 and #397.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc), through #398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 17, 2026
…-root

Brings in #394's one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held (4bb19dc), through #398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 4 at 32f4abf: B. Held-file exec and verified-byte binding proven; tampers caught.
Fix: the swapped-Runtime test's Linux subcase assumes GNU mktemp, so it fails on every Mac host (install-bootstrap-test.py:1112).

Brings in, through #397 and #395, #394's statement that the host lock
proves possession, not that the holder is the gateway (13cdcad), and
#395's hermetic default-command smoke test (630efc6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
The Linux case of
test_runtime_swapped_after_its_checksum_check_is_not_run_or_installed
used the host's own mktemp and then required the installer's scratch
folder to be under the fixture TMPDIR. GNU mktemp -d uses TMPDIR, but
Apple's mktemp -d ignores it and uses the per-user temp folder, so the
case failed on every Mac host (install-bootstrap-test.py:1112). The
Linux case now uses mock_gnu_mktemp, as the macOS cases use
mock_apple_mktemp, with a template form that GNU and BSD mktemp both
accept. The test no longer depends on the host's mktemp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

da63c235: the swapped-Runtime test's Linux case now mocks a GNU-like mktemp (mock_gnu_mktemp), so it no longer uses the host's. With an Apple-like mktemp stub on PATH it failed before at install-bootstrap-test.py:1112 and the full suite now gives Ran 98 tests ... OK (skipped=4) with the stub and with GNU mktemp.
Not run on a real Mac: no CI job runs this suite on macOS (only source-gate, Linux).


Generated by Claude Code

SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
Brings in #398's portable swapped-Runtime test (da63c23: the Linux case
mocks a GNU-like mktemp instead of using the host's), and, through #397
and #395, #394's statement that the host lock proves possession, not
that the holder is the gateway (13cdcad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
SashaMIT pushed a commit that referenced this pull request Oct 18, 2026
…-root

Brings in #398's portable swapped-Runtime test (da63c23: the Linux case
mocks a GNU-like mktemp instead of using the host's), and, through #397
and #395, #394's statement that the host lock proves possession, not
that the holder is the gateway (13cdcad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 5 at da63c23: PASS (A); independent review PASS. macOS installer suite passed twice (98 pass, 5 skip); swapped-Runtime tamper fails as intended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants