Repository navigation
Conversation
Record the install, start, /healthz, Home URL, data directory, lock, stop, and update interface a desktop launcher can rely on, and lock the /healthz body in the existing gateway test. Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
|
Independent alignment audit of runtime PRs #394–#398 and launcher PRs #4–#14. No code was changed. Full report follows. The same text is in the auditor workspace as AUDIT.md. Law read from origin/develop at 55dcb79. The open PRs branched from 75dcb4f. Grades are from the raw diffs, then the launcher tip 3f8097fe and the develop functions those diffs describe. Overall grade is the lowest principle. Principles 4, 7, 8, 13, 17, and 19 are A on every PR: these diffs do not touch capsule APIs, WebSpaces, agent grants, design tokens, or consequence classes. Grade cardRuntime: #394 C, #395 C, #396 C, #397 D, #398 D.
What CI does not checkCI compiles and runs doubles. It does not check that the bytes a person installs are the bytes a signed head names. Chain: pin file → HTTPS release-head.json → HTTPS install.sh → bash → signed release.json → URL /artifacts/elastos- → SHA-256 in that release → journal → sources.json head CID → later elastos update over Carrier. F1 — S1 — The pin does not bind the bytes. P2, P10, P11, P15. Launcher #9, #10, #13. Tip 3f8097fe.planRuntimeInstall returns fallback when the unsigned head is missing or disagrees (runtimeInstall.ts:60-91). A fetch error becomes that missing head (164-168). A matching plan that throws, other than the placeholder error, downloads again (179-199). Both downloads are the same URL. The pinned flag only changes the filename. runtimeInstall.ts:233-247: async function runInstallerStep(deps, onProgress, pinned) {
const script = await deps.fetchText(deps.pin.fallbackInstallerUrl);
assertSignedInstaller(script);
const scriptPath = path.join(dir, pinned ? 'install-pinned.sh' : 'install-fallback.sh');
deps.writeFile(scriptPath, script);
await deps.runInstaller(scriptPath, ['--install-only']);
}assertSignedInstaller (lines 14-31) searches for MAINTAINER_DID and HEAD_CID. The error says the installer is not signed. It does not run Ed25519. A script without those tokens is accepted. parseReleaseHead (41-66) compares signer_did, channel, version, and latest_release_cid as strings. It does not verify the envelope. scripts/runtime-smoke.sh:83-114 does the same compare and the same curl. On a match it checks that version output contains the pin. On a mismatch it still runs the script and skips that check (124-130). This is fallback downgrade, pin bypass (--head-cid is never passed), MITM of the bootstrap (TLS is the only binding on the script; a replaced script never reaches the runtime verifier), and TOCTOU (head fetch, script fetch, write, bash, with no hash after the write). #6 records a decision to fall back if the pinned install fails, and also says to verify the signature. PRINCIPLES.md §10 and §11 still require one path and a stop. The code does not verify the signature, and the two plans are the same bytes. Fix: one plan. Verify the head. On mismatch or install error, stop and name the field. Delete the fallback action. F2 — S1 — Version containment accepts the wrong tree. runtimeInstall.ts:85-89, 149-161, 202-206.installedVersion.includes(pin.version) plus a present components.json returns already-installed before any publisher contact. The string 0.8.0-alpha.20 contains 0.8.0-alpha.2. resume-setup runs elastos setup on that binary. After fallback, the version check does not run. The unit test sets latest_release_cid equal to headCid (test/runtime-install.test.js:40-48), so it cannot see the field mix-up in F3. Fix: full-string equality, and sources.json head_cid equals the verified pin. F3 — S2 — Head CID and release CID are different objects.publish-release.sh sets latest_release_cid to RELEASE_CID, then HEAD_CID to the ipfs add of release-head.json. The launcher compares pin headCid to latest_release_cid. A real head CID fails closed into F1's fallback. A release CID can pass and still be the wrong value for update --rollback-to, which release_cmd.rs treats as a head override. develop scripts/install.sh:50 stamps HEAD_CID and line 1392 writes it to sources.json. Those are the only uses. The downloaded head is signature-checked. Its bytes are not required to hash to the stamp. #12 stores the Head CID: line in ~/.elastos-launcher/previous-head.json with no signature (runtimeUpdate.ts:50-56, 114-128) and passes it to --rollback-to (88-96). The runtime signature check is what refuses a tampered CID. The test double returns QmPrevious and never runs it. Fix: one pin field, the CID of the verified head bytes. Pass --head-cid. Require the installed Head CID to match. Store that value for rollback. F4 — S2 — Stop and exit 75. P11, P18.develop main.rs:1307 and the SIGKILL at main.rs:1320: gateway is its own process group; SIGTERM signals the group, waits 15s, then SIGKILL. gateway_site.rs:432 returns OK. #394's test locks those bytes. host_lock.rs:239 exits 75 also when the exe path cannot be read, ends in " (deleted)", moved, or cannot be re-hashed. #394's contract text says only that the binary was replaced. Launcher runtimeGateway.ts:214-217 spawns detached. Stop SIGKILLs the pid only (180-191, 238-241). The smoke kills the group (runtime-smoke.sh:186-194). The first failed health poll counts as stopped (182-186) while the lock can still be held. isHealthOk trims the body (45-47); any local OK on port 8090 is treated as Home (151-161). Exit 75 restarts are capped at 3, and every 75 is labeled an update. Fix: signal the process group. Stop only after the pid is dead and health has failed twice. Restart on 75 only for a replaced binary. F5 — S2 — Intel is three names and three gates. #395, #397, #398. P10, P11, P12, P15.Names: darwin-x64, x86_64-darwin, darwin-amd64. release-signer.py:1084 still admits only x86_64-linux, aarch64-linux, aarch64-darwin. #397 makes package-release-binaries.sh print darwin-amd64 for Darwin-x86_64. It used to exit 1. CI only runs --print-platform. The script will now package that host. #398 detect_platform selects x86_64-darwin. A missing key still dies with "No release available for platform". README, GETTING_STARTED, and INSTALL say the signed release supplies the Intel download. MAC.md in the same PR says the signer still refuses it. #394 still says Intel is outside the install set. The #398 CI check slices install.sh before "# ── Main" and runs detect_platform. It does not fetch a signed release. At audit time, checks on #395, #397, and #398 were pending. No finished native /healthz log was available. #394's earlier rollup was green and does not build Intel. Fix: keep both scripts refusing Intel until the signer, components.json, and a signed artifact admit the platform together. Make the README match MAC.md. Keep the native smoke labeled as a build smoke. F6 — S2 — Launcher #5 passwordless sudoers. P3.pc2Manager.ts in that diff writes /etc/sudoers.d/elastos with NOPASSWD for wg-quick, NOPASSWD:SETENV for awg-quick, killall amneziawg-go, and rm -rf /var/run/amneziawg/. visudo -c deletes a bad file. SETENV crosses the caller environment into a root command. notarize.js in the same diff waits and throws unless Apple accepts. #14 says this stack is 1.2.7 and merging #5 conflicts in package.json. Fix: land #5 alone. Drop SETENV unless awg-quick requires one named variable. Rebase the runtime stack after it. Keep this file off the runtime install path. F7 — S3 — Proof CI will not refuse. P12.runtime-smoke.yml:13 is continue-on-error. #14's windows-wsl job exits 0 when wsl is missing. #395's source-gate test drives a Python stub; the shell smoke can accept OK from another listener, and command substitution hides a trailing newline the Rust test rejects. #12 rollback is a double. #14 marks a Linux click path done without the log in the diff. #396 was not executed on Windows. Nothing refuses XDG_DATA_HOME on /mnt/c. The KVM paragraph matches develop prepare_browser_linux_host and browser-vm-linux-network.py (systemd unit and udev rule). --install-only returns before that helper. Fix: required Linux and macOS smokes. A Windows skip fails the job or is absent, and does not exit 0. Attach the Intel log and a two-head rollback log. Add a test where head CID and latest_release_cid differ and the install stops. F8 — S3 — What #394 freezes, and what it does not.Matches develop for addr 127.0.0.1:8090, no browser open, health body OK, /home/, data dirs, the lock error text, the two update stdout lines, and Carrier update in release_cmd.rs. sources.rs:15 and :41 fall back to /tmp when home is missing. The new test does not take the lock or exit 75. install.sh json_get evals a Python expression that interpolates PLATFORM. Fix: make the /tmp fallback a hard error. Say in the contract which rows are specified and not yet tested, or add those tests. F9 — S3 — Preflight fetches the publisher. preflight.ts:129-137, added in #13.A cold open GETs release-head.json and treats failure as offline. That is a publisher contact, not a telemetry SDK. electron-log stays local. PC2 still checks api.github.com on the PC2 target. Fix: decide offline only during an install the user started. F10 — S4IPC remains pc2:* (index.ts). contextIsolation is true and nodeIntegration is false (index.ts:43-46). There is no will-navigate deny. #4 quotes a gateway ready line develop does not print; #394 has the real line. Uninstall confirms, then rmSync recursive on the data dir (runtimeTarget.ts:141). Blind spotsCID equality in fixtures. continue-on-error. Stub versus native /healthz. One canary head cannot prove rollback. Electron kill versus the smoke's group kill. No Ed25519 test on the pin. /mnt/c and /tmp data dirs. #394 and #398 are separate stacks. Missing evidenceLinux click-path log. Finished gateway-smoke-macos-intel log. Windows or WSL2 execution. A second signed head on a real rollback. This comment is the diff review. It is not a merge approval. Fix order
After 1–4 the F grades can move to C, and to B only with a required smoke log and a two-head rollback log. They do not reach A on stubs. #397 and #398 stay at the refusal until the signer admits the platform. #394 principle 15 can move to B after fix 7, and stays short of A until the lock, exit 75, and data-dir rows have tests. Stopped here. No product branch was modified. |
Exit 75 stays the launcher restart for a replaced binary, a moved path, or a file that changed and then could not be verified. A process that cannot read its own executable path exits 1, and the contract says the launcher leaves that process stopped. Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
The release id is x86_64-darwin and the package key is darwin-amd64. The launcher calls that host darwin-x64. The signer still refuses x86_64-darwin, and this tree has no Intel download. Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
Re-audit — launcher tip #27 and runtime #394–#398Date: 2026-10-16. Same method as the earlier card on this pull request. No code was changed. No audited branch was updated. Law is unchanged: Heads read for this card:
The earlier grades for launcher #4–#14 stand as grades of those diffs: #4 C, #5 D, #6 D, #7 C, #8 C, #9 F, #10 F, #11 D, #12 D, #13 F, #14 D. The tip below is what a person would run. Grade cardRuntime, current tips
#397 and #398 move from D to C. The signer still refuses Composed runtime tip (#394 → #395 → #397 → #398, head Launcher #15–#27
Composed launcher tip
|
| Principle | Grade |
|---|---|
| 1 Local first | B |
| 2 Stable identity | C |
| 3 No ambient authority | D |
| 5 Small trusted core | B |
| 6 Boundaries | C |
| 9 HTTP edge | C |
| 10 One path | C |
| 11 Fail closed | C |
| 12 Docs, code, tests | D |
| 14 Public names | B |
| 15 Signed content | D |
| 16 UI is not authority | C |
| 18 Isolation | C |
| Overall | D |
The earlier composed tip was F because a pin mismatch still installed. That install path is closed in the app. The tip is D because the required smoke is red, the Windows rootfs is unsigned, ELASTOS_RUNTIME_PIN replaces the trust root, and the #5 NOPASSWD:SETENV line is still in src/main/pc2Manager.ts.
Earlier findings, checked in raw code
F1, pin mismatch still installed — fixed on the app path, broken on the proof path. planRuntimeInstall has no fallback action (runtimeInstall.ts). A refused plan throws PinMismatchError. runInstallerStep requires installerSha256, hashes the download, writes ~/.elastos-launcher/install.sh, hashes the bytes it reads back, then runs bash with --install-only --head-cid --maintainer-did. verifyReleaseHead checks the pinned DID and the Ed25519 signature before that download (releaseHead.ts). The smoke exits 1 when the pin fields disagree (runtime-smoke.sh:129-131).
The required job does not reach that hash. The flatten at runtime-smoke.sh:62-69 copies channel, version, head CID, DID, gateway, and installer URL, and drops installerSha256. Line 136 then reads that key from the flat file. Run 38858365277, job smoke (linux-x64) 116834803834, on merge 71a1e25 of tip 22854830:
publisher channel=canary version=0.8.0-alpha.20 release_cid=QmTAEJ8DAprsv5PbWcudofQwXrzL7ZjSgmhRmU8nqmwirf head_cid=QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt
Pinned release matches the publisher. Verifying the signature and installer hash.
KeyError: 'installerSha256'
The signature script never ran. The header at runtime-smoke.sh:6-7 and .github/workflows/runtime-smoke.yml:3-7 still say the script falls back to the hosted installer. The code stops. The comment describes the old path.
F2, version substring — fixed on the compare, with a local shortcut. installedVersionMatches is exact equality with ElastOS Runtime v${pin} (runtimeInstall.ts:27-30). already-installed and resume-setup return when that version and sources.json head_cid match the pin, before any publisher fetch (runtimeInstall.ts:171-183). A local binary and a local sources file can satisfy that without a new signature check.
F3, head CID versus latest_release_cid — fixed on the install path, open on rollback. The pin headCid is QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt. The live log above shows that value as the smoke helper’s UnixFS CID of the publisher bytes, and shows latest_release_cid as the old QmTAEJ8… value. The launcher compare uses head.headCid (runtimeInstall.ts:76). #398 bind_verified_head_cid runs after verify_signature, refuses a stamp that names other bytes, and stores the CID it computed. Three copies of that CID helper exist (launcher releaseHead.ts, smoke Python, install.sh). This run shows the smoke copy and the pin agree on the live file. It does not show ipfs add. runtimeUpdate.ts:50-56 and :114-128 still store whatever token follows Head CID: in ~/.elastos-launcher/previous-head.json and pass it to --rollback-to.
F4, exit 75 and the wrong process — mostly fixed, and the contract is wider than the launcher. #394 exits 1 when the executable path cannot be read, and exits 75 for replaced, moved, unverifiable, and re-hash failure. The launcher restarts only when the code is 75 and the text matches was replaced on disk or changed on disk, and it excludes could not be read, could not be verified, and could not be re-verified (runtimeGateway.ts:30-34). A move (moved from) does not match, so it does not restart. The #398 contract table still says exit 75 covers a move and a failed verify, and that the launcher starts the gateway again. Stop waits until the pid has exited and /healthz has failed twice (runtimeGateway.ts:181-196), then signals the process group (:37-47). A foreign lock plus any trimmed OK on 127.0.0.1:8090 still opens Home (:206-216). signalProcessGroup swallows a failed kill.
F5, Intel names — the refusal is now the documented behavior. #398 selects x86_64-darwin on Intel and the docs say the signer refuses that id, so lookup stops with no Intel download. #397 prints package key darwin-amd64 and the test says that name is not a signed download. scripts/release-signer.py is unchanged. Intel is not an installable signed release.
F6, sudoers SETENV — still present. src/main/pc2Manager.ts:1034 still writes NOPASSWD:SETENV for awg-quick, plus killall amneziawg-go and rm -rf /var/run/amneziawg/. None of #15–#27 edit that line.
F7, smoke evidence — the old green log is a different commit. continue-on-error is gone. The checklist green run is 0102b48 / run 38850728295, whose smoke still used a substring version check and did not read installerSha256. Tip 22854830 is red on Linux and on Windows for the KeyError above. macOS arm64 was still queued when this card was written.
F8, installer stamp — fixed on the #398 tip only. bind_verified_head_cid is the fix. The /tmp data-dir fallback and json_get evaluation are outside these diffs.
F9, publisher fetch on open — still present. preflight.ts:206-214 GETs {publisherGateway}/release-head.json inside collectPreflight. Fix it’s online action does the same fetch, which matches a button the person clicked.
F10, IPC name and uninstall — still present, with a new Reset path. IPC includes pc2:resetRuntime. Reset confirms, then fs.rmSync(..., { recursive: true, force: true }) on the binary, the data directory, and install-stage.json (runtimeUninstall.ts:67-75).
New blind spots
N1 — S2 — unsigned Windows rootfs. windowsHost.ts:22-23 and scripts/windows-elastos-distro.ps1:12 download https://cloud-images.ubuntu.com/wsl/releases/24.04/current/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz. The product writer stores the whole body under ~/.elastos-launcher/ with no hash and no size cap (windowsHost.ts:206-214). The CI script requires 50MB, then wsl --import. Run 116834803984 downloaded 356,739,129 bytes, imported distro ElastOS as WSL 2, and printed kvm=yes. It then hit the same KeyError. The CI script runs wsl.exe --unregister ElastOS before import (windows-elastos-distro.ps1:33-35). On that runner the distro was absent (WSL_E_DISTRO_NOT_FOUND, unregister_exit=-1) and the script continued. The product import does not unregister. launchController.ts:271-273 treats a Windows Fix it as ready as soon as fixWindowsOnThisComputer() returns. The ready copy says the button cannot finish the install inside that environment.
N2 — S2 — stale lock delete. assertStaleLockPath checks the basename host-process.lock, the parent basename elastos, and the substrings /.pc2/ and /pc2.net/ (runtimeRecovery.ts:64-74). It does not lstat, realpath, or reject a symlink. pidAlive is process.kill(pid, 0) (:129-136), so any live pid blocks the delete, including a recycled pid. collectFailureFacts snapshots the lock text. fixCollectedFailure later fs.rmSyncs that path (:165-168). A new gateway that replaced the lock file between the snapshot and the delete loses the new file. Unlink does not drop a flock held on the previous inode. Fix it does not signal the pid. The port copy says ElastOS will not close other programs.
N3 — S3 — support bundle scrub is a line filter. scrubSupportText drops lines matching attach_secret, the coords filenames, private key, and BEGIN … PRIVATE KEY, and rewrites bearer and password= (preflight.ts:129-136). A secret that misses those patterns stays in the file. saveSupportBundle refuses another filename and refuses /.pc2/, /pc2.net/, and /elastos/ (:151-157), then writes mode 0600 on the Desktop when that directory exists, otherwise in the home directory (:141-143).
N4 — S2 — the pin file is replaceable from the environment. runtimePinCandidates tries ELASTOS_RUNTIME_PIN before the packaged runtime-pin.json (runtimePin.ts:106-112). Verification trusts that file’s DID, gateway, installer hash, and head CID. installerChildEnv deletes only ELASTOS_MAINTAINER_DID, ELASTOS_PUBLISHER_GATEWAY, and ELASTOS_HEAD_CID (releaseHead.ts:313-318).
N5 — S3 — Fix it copy and the call disagree. The interrupted screen says setup continues without downloading again (runtimeRecovery.ts:208-210). resumeSetup is installRuntimeOnThisComputer() (:164), which can fetch the publisher and the installer. The hash check still applies on that path.
N6 — S3 — Reset and Fix it path checks are string checks. Basename elastos, basename elastos for the binary, basename install-stage.json, and a .pc2 / pc2.net substring (runtimeUninstall.ts:6-16, :46-51). The Windows .pc2 test uses path.sep. A forward-slash path misses it. The recursive delete follows the path Node is given. The code does not resolve symlinks first. Stop failure text did not start it refuses the delete.
Evidence still missing
- A green required smoke on tip
22854830. The current Linux and Windows jobs are red withKeyError: 'installerSha256'. The signature verifier and the installer hash did not run in that log. - The macOS arm64 job on the same run was queued at this reading.
- A finished
gateway-smoke-macos-intelsuccess on Smoke the gateway health probe on Intel Mac #395, Name the Intel Mac release package darwin-amd64 #397, or Stop Intel Mac before release lookup and pin the verified head CID #398. Those checks had no success conclusion in this snapshot. Source-gate on those PRs is green. /healthzinside the ElastOS WSL distro. The Windows job imported the distro and died in the smoke script before the gateway.- A byte compare of pin
installerSha25618768ef071a9f921fe0c1d902926c138e86f925dbaf5dfb9185fecb8789d6714withhttps://elastos.elacitylabs.com/install.sh. ipfs addof the liverelease-head.jsoncompared with the local UnixFS helper.- Two signed heads on a real
elastos update --rollback-to. - This comment is the review of the diffs and of those two failed logs. It is not a merge approval.
Fix order
- Put
installerSha256on the flattened smoke pin, delete the fallback sentences in the script header and the workflow, and attach a green log of tip22854830that includes the signature line andinstall.sh SHA-256 matches the pin. - Refuse
ELASTOS_RUNTIME_PIN, or require it to be the packaged pin. Keep the three installer environment deletions. - Hash the WSL rootfs, and return ready only after a fresh distro check. Keep
wsl --unregister ElastOSon the CI runner. - Re-read the lock immediately before unlink, and unlink only when that read still shows a dead pid and the path is not a symlink. Use the same realpath rule before Reset’s recursive delete.
- Change the Stop Intel Mac before release lookup and pin the verified head CID #398 exit-75 sentence so a restart happens for a replaced binary and a changed hash. A move, an unreadable path, and a failed re-hash stay stopped, matching
runtimeGateway.ts. - Leave the publisher fetch on the Fix it online action. Remove it from the cold
collectPreflightpass. - Store a head CID only after
elastos source showand a signature check the runtime already performed. An arbitraryHead CID:token stays out of--rollback-to. - Drop
SETENVfrompc2Manager.ts:1034in its own change.
After 1–4 the composed launcher tip can move from D to C, and to B only with the green smoke log and a two-head rollback log. #397 and #398 stay C until the signer admits x86_64-darwin and a signed Intel artifact exists. No grade here is a merge approval.
Third audit — launcher tip
|
| PR | Overall | Change since the re-audit |
|---|---|---|
| #394 | C | Same commit. Exit 75 in the contract still includes a move and a failed re-hash, and says the launcher starts again. |
| #395 | C | gateway-smoke-macos-intel is now success. The job probes the binary that job built. The signer still refuses x86_64-darwin. |
| #396 | C | Same commit. Still outside the #394 stack. |
| #397 | C | Same commit. Package key darwin-amd64 is still not a signed download. Intel smoke on this tip is success. |
| #398 | C | Same commit. bind_verified_head_cid still runs after the signature check. Intel lookup still stops. |
Composed runtime tip (#394 → #395 → #397 → #398, head 355bddd5): C. #396 stays C.
Launcher #28–#36
Earlier overalls stand: #4 C, #5 D, #6 D, #7 C, #8 C, #9 F, #10 F, #11 D, #12 D, #13 F, #14 D, #15 C, #16 B, #17 D, #18 C, #19 D, #20 B, #21 B, #22 B, #23 C, #24 C, #25 C, #26 D, #27 B. Those are grades of those diffs. The tip grades below replace them for what a person would run.
| PR | Overall | What sets it |
|---|---|---|
| #28 | B | The smoke flatten keeps installerSha256. The header says install stops. |
| #29 | B | The WSL rootfs is release 20240423, with a SHA-256, an exact size, and a 400MB cap. |
| #30 | C | Production refuses an env pin that changes the trust root. NODE_TEST_CONTEXT still selects dev mode. |
| #31 | B | Rollback stores a CID only after verifyReleaseHead on the bytes at /ipfs/<cid>. |
| #32 | B | already-installed and resume-setup run only after the signed head matches the pin. |
| #33 | B | Fix it re-reads the lock and refuses a symlink. Reset walks ancestors with lstat. |
| #34 | C | Windows Fix it is ready only when /healthz is OK. That probe is any OK on port 8090. |
| #35 | B | Home opens for a foreign lock only when that pid’s command is an elastos gateway on 127.0.0.1:8090. |
| #36 | C | The smoke sets ELASTOS_HEAD_CID and drops --head-cid. The Install button still passes --head-cid. |
Composed launcher tip 2e8cd84
| Principle | Grade |
|---|---|
| 1 Local first | B |
| 2 Stable identity | B |
| 3 No ambient authority | D |
| 5 Small trusted core | B |
| 6 Boundaries | C |
| 9 HTTP edge | B |
| 10 One path | D |
| 11 Fail closed | B |
| 12 Docs, code, tests | D |
| 14 Public names | B |
| 15 Signed content | B |
| 16 UI is not authority | C |
| 18 Isolation | B |
| Overall | D |
The re-audit tip was D because the required smoke was red, the rootfs was unsigned, and an env pin could replace the trust root. Those three are fixed on the smoke path. The tip stays D because pc2Manager.ts:1034 still writes NOPASSWD:SETENV, and the Install button still passes a flag the pinned installer rejects.
Re-audit findings, checked in raw code
F1 / N2 smoke flatten — fixed, and the log shows it. scripts/runtime-smoke.sh copies installerSha256 into the flat pin. Run 38863773128 job smoke (linux-x64) 116851807624:
publisher channel=canary version=0.8.0-alpha.20 release_cid=QmTAEJ8DAprsv5PbWcudofQwXrzL7ZjSgmhRmU8nqmwirf head_cid=QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt
Release head signature matches the pinned maintainer
install.sh SHA-256 matches the pin
Installed version: ElastOS Runtime v0.8.0-alpha.20
Installed head CID matches the pin
/healthz is OK
/home/ HTTP 200
Gateway stopped cleanly
The installer log on that job also has [OK] Signature verified for the head and for the release, then [OK] Checksum and version match the signed release. Job smoke (darwin-arm64) 116851807398 has the same sentences plus Platform: macOS 14.8.9 on Apple silicon (aarch64-darwin). Job smoke (windows-wsl) 116851807680 has Rootfs SHA-256 matches the pin, kvm=yes, the same signature and hash lines, /healthz is OK, /home/ HTTP 200, and smoke_exit=0.
F2 local skip — fixed. planRuntimeInstall returns already-installed or resume-setup only after a verified publisher head is in hand (runtimeInstall.ts). A version string and a local sources.json no longer skip the signature check. The test refuses a head whose signature does not match. The local binary’s own SHA-256 is still not compared with the signed release.
F3 rollback token — fixed in the launcher, unrun as a runtime command. verifiedHeadForRollback accepts a Qm… token, fetches {publisher}/ipfs/{cid}, runs verifyReleaseHead, and stores the CID only when the computed CID equals the token (runtimeUpdate.ts). QmPrevious is refused before elastos update. I fetched the public objects, without running elastos update: https://elastos.elacitylabs.com/ipfs/QmUmMZD3yNs4C7JvtichoZ727gN2ntXNnCDF1QTHkNKhXt is the same 624 bytes as release-head.json, and the UnixFS helper’s CID matches the pin. The previous head in that payload is also served at /ipfs/<cid> and matches its own CID (canary 0.8.0-alpha.19). There is still no log of elastos update --rollback-to against a real binary.
F4 foreign OK and exit 75 — the open is fixed; the contract sentence is not. #35 opens Home for a held lock only when foreignListenerIsElastos is true: integer pid, addr exactly 127.0.0.1:8090, pid alive, command matches elastos or elastos.exe and the word gateway (runtimeGateway.ts). A python3 -m http.server command leaves Home closed. signalProcessGroup returns on ESRCH and throws on any other kill error. #394/#398 still say exit 75 covers a move and a failed verify, and that the launcher starts again. The launcher still restarts only for was replaced on disk or changed on disk.
F5 Intel — unchanged runtime, with the missing smoke log now green. gateway-smoke-macos-intel is success on #395, #397, and #398. That job starts the binary it built. scripts/release-signer.py still refuses x86_64-darwin. The installer still selects that id and stops when the signed release omits it.
F6 SETENV — still present. src/main/pc2Manager.ts:1034 still writes NOPASSWD:SETENV for awg-quick, plus killall amneziawg-go and rm -rf /var/run/amneziawg/. None of #28–#36 edit that line.
F7 smoke evidence — the tip run is green. The required jobs on 2e8cd84 succeeded. The file docs/RUNTIME_MIGRATION.md still points the checklist at run 38862931438.
F8 head stamp — still the #398 behavior, and the smoke exercised it. The published installer reads HEAD_CID="${ELASTOS_HEAD_CID:-__HEAD_CID__}" (scripts/install.sh:52 on 355bddd5) and bind_verified_head_cid refuses a stamp that names other bytes. The green log reached Installed head CID matches the pin.
F9 cold publisher fetch — still present. preflight.ts:206-214 still GETs release-head.json inside collectPreflight.
F10 Reset — symlink and forward-slash PC2 checks are in the tip. assertNoSymlinkAncestors lstats each path component and refuses a symlink (runtimeUninstall.ts). mentionsPc2 normalizes backslashes, so C:/Users/tester/.pc2/elastos is refused. A failed lstat is skipped and the walk continues. The recursive delete still follows.
N1 rootfs — fixed on the path the Windows job ran. URL is …/24.04/20240423/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz, SHA-256 8251e27ffff381a4af5f41dcb94d867de3e0d9774a9241908ab34555d99315ea, size 356739129, cap 400MB (windowsHost.ts). downloadToFile refuses any other URL, stops past the cap, and calls assertPinnedRootfs before the write. The Windows log says Rootfs SHA-256 matches the pin. ensureElastosRootfs returns a cached file without hashing when the caller omits read and accept. The product path passes both.
N4 env pin — fixed for production, open for one env flag. pinLoadMode returns dev when NODE_TEST_CONTEXT is set, before it looks at app.isPackaged (runtimePin.ts:121-129). Dev mode still takes ELASTOS_RUNTIME_PIN first and does not call overrideKeepsTrustRoot. A packaged app whose environment contains that variable gets the old trust-root bypass. Production mode throws ELASTOS_RUNTIME_PIN cannot replace the packaged trust root when the DID, gateway, installer hash, or channel CIDs differ.
N5 Fix it copy — still ahead of the call. The interrupted screen still says setup continues without downloading again (runtimeRecovery.ts). resumeSetup is installRuntimeOnThisComputer(), which now fetches and verifies release-head.json and skips the installer when the signed head matches. The installer bytes are not downloaded again. The head document is.
New blind spots in #28–#36
N7 — S2 — the Install button and the pinned installer disagree. runtimeInstall.ts:279-286 runs bash install.sh --install-only --head-cid <pin> --maintainer-did <pin>, and installerChildEnv deletes ELASTOS_HEAD_CID. On runtime 355bddd5, scripts/install.sh:1188 is *) die "Unknown option: $1". There is no --head-cid option. The head stamp is the environment variable at line 52. #36 changed the smoke to:
env -u ELASTOS_MAINTAINER_DID -u ELASTOS_PUBLISHER_GATEWAY \
ELASTOS_HEAD_CID="${head_cid}" \
bash "${installer}" --install-only --maintainer-did "${maintainer}"That is the invocation the green run executed. test/runtime-install.test.js still expects --head-cid and expects ELASTOS_HEAD_CID to be absent. test/smoke-required.test.js expects the smoke to contain no --head-cid. Both tests can pass. The smoke does not exercise the Install button. A click dies with Unknown option: --head-cid after the hash check, so the wrong bytes are not installed. The one-click path does not finish.
N8 — S3 — Windows Fix it treats any OK as ready. launchController.ts:271-279 calls windowsFixResult, which returns ready when isHealthOk is true. It does not call foreignListenerIsElastos. Enabling WSL or importing the distro does not start the gateway, so this path stays still-blocked until something answers OK. The product copy still says the button cannot finish the install inside the distro.
N9 — S3 — the foreign check reads the command line, not the socket. A live pid whose ps or /proc/pid/cmdline text matches elastos gateway is enough. The listener on port 8090 is not tied to that pid. On Windows, ps failure returns a null command and Home stays closed.
What is left to reach B
The composed launcher tip reaches B when every principle below D is at least B. That is four changes:
- Pass the head the way
install.shaccepts it, inruntimeInstall.ts, not only in the smoke. SetELASTOS_HEAD_CIDto the verified pin after clearing a parent value. Stop passing--head-cid. Point the unit test at that same invocation. One Install-button log, or a test that runs the pinned script’s option parser, has to show the install get past argument parsing. - Remove
NOPASSWD:SETENVfrompc2Manager.ts:1034in its own change. - A packaged app stays in production pin mode when
NODE_TEST_CONTEXTis set. Dev override stays on an unpackaged app. - Finish the Windows journey inside the distro, or keep Fix it on
still-blockeduntil that journey exists, and useforeignListenerIsElastosfor that/healthzcheck. The interrupted-screen sentence should say the signed head is checked again.
The composed runtime tip reaches B when the #398 exit-75 sentence matches the launcher: restart for a replaced binary or a changed hash; a move, an unreadable path, and a failed re-hash stay stopped. Intel stays C until the signer admits x86_64-darwin and a signed artifact exists. A green local /healthz on a built binary does not change that.
What is left to reach A
B is the floor. A also needs this evidence and these residuals closed:
- An Electron Install click on the same commit as the smoke, with the signature line and
Installed head CID matches the pinin that click’s log. elastos update --rollback-toof the served0.8.0-alpha.19head, then a return to0.8.0-alpha.20, on a real binary. The two head objects are on the publisher. The command log is not.- The cold
collectPreflightfetch ofrelease-head.jsonremoved, or moved to an action the person started. The support-bundle scrub is still a line filter (preflight.ts:129-136). - The foreign-gateway check bound to the process that accepted the
/healthzconnection. - The lock unlink and the Reset delete repeated against the same inode after the
lstat, with a failedlstatrefusing the delete. ipfs addof the live head compared with the local UnixFS helper, or a written acceptance that this helper is the publisher’s definition. This card shows the helper, the pin, and the served bytes agree. It does not show Kubo.- The tag build notarized. The checklist item is still open.
- Intel A only with a signed
x86_64-darwinartifact the signer admits. Until then the honest grade is the refusal.
No grade here is a merge approval.
A moved path, an unreadable path, and a failed re-hash exit 1. The launcher leaves that process stopped. The contract uses the same rule. Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
Restore the binary watch to the develop exit-75 behavior. This pull request no longer changes which supersession paths restart. /healthz returns the two bytes OK only when this process owns host-process.lock. Passkey registration and sign-in use http://localhost:8090 and refuse an IP-literal origin. The launcher contract states those facts and stops using English stdout as a supervisor interface. Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
The configured-host /healthz check writes the gateway lock before it expects HTTP 200. Local guest fixtures use rp id localhost, matching the origin the session grant accepts. Co-authored-by: Sash <SashaMIT@users.noreply.github.com>
elastos serve holds host-process.lock as serve, and its supervisor can start a gateway in the same process for elastos room open. The readiness change made that gateway take the lock a second time, so it stopped at start with "another ElastOS host already owns" naming its own pid. The gateway now keeps a record that already names this process. Its /healthz returns 503 because the role is serve, not a Home gateway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…efused Every release takes the relying party id from the page host, and browsers refuse an IP address as that id, so a stored passkey names the host where it was made. A test signs a stored localhost owner passkey in at three ports and a public passkey at its https host, and refuses both at foreign, IP-literal and look-alike hosts without a state change. The refusal is HTTP 403 with plain text that names http://localhost:<port>/home/ for a loopback page, instead of "owner enrollment unavailable". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ract /healthz describes the process that answers on the port, so another gateway for a different data directory also returns OK. The contract now says what a supervisor checks before it trusts OK. It also states that --addr with a host name binds one address, so localhost:8090 listens on one address family. The healthz test adds another role, a missing lock and a string pid, and pins the gap text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
cargo-audit fails on every PR: zip 2.4.2 in elastos/Cargo.lock and in capsules/object-provider/Cargo.lock (through its elastos-server path dependency) has three advisories against ZipArchive::extract and the stream extract API. The runtime calls neither; it reads each entry itself. zip 9.0.4 is the patched release. - elastos-server uses zip 9.0.4, still without default features. 9.x has no separate flate2 feature; deflate-flate2 now enables the dependency. - Both lockfiles change only for zip: nix 0.31.3 and typed-path come in, derive_arbitrary goes. `cargo update -p zip` also moved unrelated range dependencies (windows-sys, getrandom); those lines stay as they were, and `cargo metadata --locked` accepts both files. - ZipFile::name now returns a Result. The ZIP readers report an error as "invalid ZIP archive entry". - Whole-archive extract keeps its rule for entry names. zip 9 enclosed_name turns "/a.txt" into "a.txt" and folds "x/../a.txt" into "a.txt", so an entry that extract refuses today would be written. The runtime now checks the name with the rule zip 2.x applied: no NUL byte, no root or drive prefix, no climb above the archive root. test_library_provider_extract_zip_refuses_absolute_dotdot_and_symlink_entries covers absolute, "..", inner "..", backslash and symlink entries. It passes on zip 2.4.2 and with this change, and fails on the Absolute.zip case when extract uses zip 9 enclosed_name. 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 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…lhost Passkeys work only at localhost, and a browser reaches localhost on 127.0.0.1 and on ::1. --addr localhost:<port> now binds each address the computer resolves for localhost plus 127.0.0.1 and ::1, on one port, and serves the same router on each. Another program on any of them, or any other bind error, stops the start with a message that names the address. Only an address family the computer lacks (EADDRNOTAVAIL, EAFNOSUPPORT) is left out, and at least one address must bind. A localhost that points off this computer is refused. An IP-literal --addr binds exactly as before, and the host lock still records --addr as given. When the gateway listens on localhost, a GET or HEAD for /, /home, /home/, /apps/home or /apps/home/ with a loopback IP Host gets 308 to the same path at http://localhost:<port>. Health, API, asset and app routes answer on every address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
The contract now states the localhost listeners, the redirect from a loopback IP Home page, that localhost is the one Home origin, and that the lock records --addr as given. It drops the single-family gap and names the untested Windows-to-WSL2 localhost forwarding. HOME_LOCAL_SETUP, INSTALL, MAC and WINDOWS cite it; 61180 is labelled the developer lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…nto cursor/launcher-contract-1297
…d retry (#381) Port of develop commit 5051505 (Anders Alm). The test flaked in CI with "WouldBlock" at journal.rs:4223 on PR heads that predate the fix. (cherry picked from commit 5051505) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ecord Any process that runs as the same user can rewrite the JSON record in host-process.lock. /healthz returned OK when that record named this pid with role gateway, and a gateway skipped taking the lock when the record named its pid. A forged or stale record could therefore make a process that holds no lock answer OK, or start a gateway with no host lock. This process now remembers each host lock it holds (role, device and inode of the locked file). /healthz and publish_gateway_lock_if_unowned answer from that state: the path must name the same file now, and a new open of it cannot take the lock. The record is not read. A removed or replaced lock file, a lock another process holds, and a released lock return 503. The launcher contract states what proves ownership. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
|
Generated by Claude Code |
…-1297 Brings in the zip 9 merge, the journal test cherry-pick and the lock ownership fix 3672007 from the base branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ke-1297 Brings in #394's proof of gateway lock ownership from the held flock (3672007). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
|
Round 3 review: fixes needed (B-). Host-lock and forged-owner tests pass. |
held_host_role looked up the registry, released its mutex, and then probed the lock with a new open of the file. A release and a retake by another holder could come between the two, and the probe then saw the other holder's lock and answered with this process's role. A registry entry also stood for any lock on the same file, so a guard whose open file was unlocked behind its back still counted when another holder took the lock. Each registry entry now keeps a second descriptor of the open file that holds the flock. The proof keeps the registry mutex from the lookup to the end of the probe, and a guard removes its entry under that mutex before it unlocks. The probe checks that the path names the same file, that a new open cannot take the lock, and that the entry's own open file can take it again (which is a no-op only for the current holder). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
A managed runtime ran as the subordinate of a gateway when the JSON record in host-process.lock named this pid as a gateway, or, for a Home terminal, named its parent pid as a gateway. Any process of the same user can write that record, so a forged or stale record opened the lane while another host held the lock. The lane now uses the same proof as /healthz. In the gateway process, holds_gateway_host_lock checks the flock it holds. A Home terminal gets a descriptor of the gateway's locked open file and the gateway pid when the gateway starts it; it must be that gateway's direct child, and that descriptor must hold the flock now (the path names the same file, a new open cannot take the lock, and the inherited open file can). After the gateway releases the lock, or another holder retakes it, the lane is refused. The record only names the holder in the refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
active_host_process reported no active host when the lock was held but its JSON record was empty, being written, or not JSON. The update controller, principal-root upgrade and the managed runtime lane then treated a running host as stopped. The flock now decides whether a host is active; such a holder is reported with role "unknown". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
|
Generated by Claude Code |
…ke-1297 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). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ame-1297 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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
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
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
…-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
Brings in #394 4bb19dc: one-step flock ownership proof, the subordinate lane on the same proof, and a held lock with an unreadable record counting as held. host_lock.rs merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
|
Round 4 at 4bb19dc: B. One-step proof, lane proof and unreadable-record fixes all proven; the round-3 test gap is closed; CI green. |
… is the gateway Holding the flock on host-process.lock proves that a process has the lock. It does not prove that the holder is a gateway or ElastOS: any process of the same user can open the file and hold it. The launcher contract now says so for /healthz, for the Home terminal's inherited lock descriptor (ELASTOS_GATEWAY_HOST_LOCK_FD), and for the "another ElastOS host already owns" error, which names the holder from a record that can be false. Code comments in host_lock.rs, gateway_site.rs and runtime_control.rs say the same. Two tests pin the statement with real processes. In one, /bin/sleep opens the lock file and takes the flock: the gateway does not start by either path, this process's /healthz answers 503 and it gives no lock descriptor to a child. In the other, this test process takes the flock with a plain open (not as a host) and gives the descriptor and the gateway variables to a real child process; the child's Home terminal check accepts it while the lock is held, and refuses a descriptor that does not hold the lock and the same descriptor after the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
|
Generated by Claude Code |
…ke-1297 Brings in #394's statement that the host lock proves possession, not that the holder is the gateway, with its two process tests (13cdcad). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…ame-1297 Brings in, through #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) and "holds" wording (ac97a4a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
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
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
…-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
Brings in #394 13cdcad: the launcher contract states that the host lock proves possession, not that the holder is the gateway, also for the inherited ELASTOS_GATEWAY_HOST_LOCK_FD, with two process tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Brings develop to bfefe92, which fixes develop's own #430 Home link test (it now uses Library, which every platform launches). This PR's pull_request CI merges develop, so its test-elastos runs on 13cdcad / 16a132a tested develop 8ea4aac/0dbba668 and failed on that test (403 vs 200, home_system.rs:11964). No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
|
Merged develop at Generated by Claude Code |
|
Re-review at d30027e: PASS (A); independent review PASS. Only a develop merge; lock tests and the gateway-identity tamper still hold; all CI jobs green. |
…ke-1297 Brings in #394's merge of develop (d30027e). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Brings in #394's head d30027e, which merges develop at bfefe92 (develop's fix for its own #430 Home link test). No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Summary
A supervisor starts Home with
elastos gateway --addr localhost:8090. The Home origin ishttp://localhost:8090/home/. The passkey relying party id islocalhost. Registration and sign-in at that origin succeed inlocal_home_passkey_registers_and_signs_in_at_localhost(elastos/crates/elastos-server/src/auth.rs). An IP-literal origin, includinghttp://127.0.0.1:8090, is refused.Passkeys that a browser created keep working after this change. Every release takes the relying party id from the page host (
derive_rp,api/handlers/identity.rs), and browsers refuse an IP address as a relying party id, so a stored passkey names the host where it was made.passkey_from_an_earlier_release_signs_in_and_a_foreign_origin_is_refused(api/auth_gateway.rs) signs a storedlocalhostowner passkey in at ports 8090, 61180 and 18090, and a stored public-Home passkey at its https host. The same test refuses those passkeys at a foreign host,evil.localhost,127.0.0.1,[::1]and a host that only contains the relying party id, and the auth state stays unchanged.Readiness proves that this gateway holds the lock from the held lock, not from the lock file's JSON (
36720070).GET /healthzreturns HTTP 200 and the two bytesOKonly while the process that answers holds{data}/host-process.lockasgatewayorgateway-public(api/gateway_site.rshealthz->host_lock::holds_gateway_host_lock). The proof is in-process state: each host lock this process takes is remembered with its role (host_lock.rsremember_held_host_lock, removed inFileLock::drop/releasebefore the unlock). The JSON record (pid,role,addr) is not read: any process of the same user can rewrite it. The earlier head trusted that record in two places:/healthz(gateway_site.rsgateway_health_lock_matches, which compared JSONpidandrole) andpublish_gateway_lock_if_unowned(host_lock.rs, which skipped taking the lock when the JSONpidwas its own, so a forged or stale record let a gateway run with no host lock).The lock proves possession, not identity (
13cdcad0, docs and tests only; no behaviour change). Holding theflockproves that a process has the host lock. It does not prove that the holder is a gateway or ElastOS: any process of the same user can openhost-process.lockand hold it.docs/LAUNCHER_CONTRACT.md("Readiness and Home URL", lines 58-75; "Data directory and lock", lines 123-125) now states this for/healthz, for the Home terminal's inherited descriptor (ELASTOS_GATEWAY_HOST_LOCK_FD: the terminal's check proves that the descriptor holds the lock and that its parent has the given pid, not that the parent is the gateway; a same-user process that holds the lock can give the same variables and its descriptor to its own child), and for theanother ElastOS host already ownserror (given for any holder, also one that is not ElastOS; it names the holder from a record that can be false). The comments athost_lock.rs(registry,inherited_gateway_host_lock_is_held,active_host_process),gateway_site.rshealthzandruntime_control.rsparent_gateway_lock_provensay the same. The error text is unchanged because the contract pins that string for supervisors.One-step proof against the holder's own lock (
85f3142e).held_host_roleused to look up the registry, release its mutex, and then probe with a new open of the file. A release plus a retake by another holder could come between the two, and the probe then saw the other holder's lock. Now each registry entry keeps a second descriptor (try_clone, adup) of the open file that holds theflock.held_host_rolekeeps the registry mutex from the lookup to the end of the probe, and a guard removes its entry under that mutex before it unlocks, so no release or retake can come between them. The probe (description_holds_lock) requires: the path names the same device and inode as the entry's descriptor; a new open cannot take the lock; and the entry's own descriptor can take it again. A repeatedflock(LOCK_EX)on the description that already holds it is a no-op on Linux and macOS, and any other holder makes it fail, so this names the open file description that holds the lock (not the program behind it).The subordinate runtime lane uses the same proof (
eed83a72).runtime_control.rsused to open the lane when the JSON record named this pid as a gateway, or (for a Home terminal) named the parent pid as a gateway. Nowsubordinate_gateway_hostopens it only whenholds_gateway_host_lockis true in this process, or when this process is a Home terminal whose gateway still holds the lock: the gateway passesELASTOS_GATEWAY_HOST_PIDandELASTOS_GATEWAY_HOST_LOCK_FD, a descriptor of its locked open file, and keeps that descriptor open across exec only for that child (api/gateway_home_terminal.rs,gateway_host_lock_for_child). The terminal must be the direct child of that pid, andhost_lock::inherited_gateway_host_lock_is_heldruns the same three-step probe on the inherited descriptor (and marks it close-on-exec). A gateway insideservepasses no descriptor, so its terminal is refused, as before. The record only names the holder in the refusal message. As stated above, this proves possession of the lock by the parent's descriptor, not that the parent is the gateway.A held lock with an unreadable record is still held (
4bb19dcf).active_host_processreturned "no host" when the lock was held but its record was empty, being written or not JSON, so the update controller, the principal-root upgrade wait and the subordinate lane treated a running host as stopped. It now reports such a holder with roleunknown.Hostile tamper tests:
test_healthz_refuses_a_forged_or_foreign_lock_owner(api/gateway_tests/site_publication.rs): with no lock held, a record forged as this pid with rolegatewayorgateway-public, another pid, another role, a string pid, non-JSON and a missing file all return 503; a lock held by another open file (standing in for another process) with the record forged to this pid returns 503; this process holding the lock asservewith the record rewritten togatewayreturns 503; a removed-and-recreated lock file with a forged record returns 503; a released gateway lock that left its own record returns 503. While this process holds the lock asgateway, rewriting the record does not change the 200.test_the_host_lock_proves_possession_not_that_the_holder_is_the_gateway(api/gateway_tests/site_publication.rs, new): a real separate process,/bin/sleep, opens the lock file itself and takes theflockbefore exec (host_lock::test_support::HeldByAnotherProcess); the record names its pid asgateway.active_host_processreports pid = that process, rolegateway;acquire_host_process_lock(.., "gateway", ..)andpublish_gateway_lock_if_unownedboth fail withanother ElastOS host already owns ... (pid <sleep pid>, ...);held_host_roleis none,gateway_host_lock_for_childis none,/healthzis 503. After the process ends, its leftover record is 503 and the gateway takes the lock and answers 200. The test also checks the contract sentences.home_terminal_lane_proves_lock_possession_not_that_the_parent_is_the_gateway(runtime_control.rs, new): this test process takes theflockwith a plain open (not as a host, soheld_host_roleis none) and starts a real child (the test binary,home_terminal_lane_probe_child) withELASTOS_GATEWAY_OWNED_HOME_TERMINAL=1,ELASTOS_GATEWAY_HOST_PID=<its pid>andELASTOS_GATEWAY_HOST_LOCK_FDkept open across exec. The child'sparent_gateway_holds_host_lockanswerstruewhile the lock is held,falsefor a plain descriptor of the same file, andfalsefor the same descriptor after the release. The test also checks the contract sentences.gateway_takes_the_lock_when_only_a_forged_record_names_this_process(host_lock.rs): a record naming this pid does not stop the gateway from taking the real lock.release_and_retake_prove_only_the_current_holder(host_lock.rs): gateway lock held ->gateway; released -> none; retaken by this process asserve->serveonly; another holder retakes -> none; the gateway retakes ->gateway.an_entry_for_the_same_file_is_not_proof_when_another_holder_has_the_lock(host_lock.rs): the guard's open file is unlocked behind its back and another holder takes the lock; the registry entry for that file is not proof.a_release_and_retake_cannot_come_between_the_lookup_and_the_probe(host_lock.rs): a test hook between the lookup and the probe starts a thread that releases this process's lock and lets another holder take it, and waits 300 ms; the answer must not beSomefor a lock another holder took first. With the fix the release waits for the probe and the answer isgateway; afterwards it is none.inherited_gateway_descriptor_proves_only_a_lock_its_gateway_still_holds(host_lock.rs): the inherited descriptor proves the lock while the gateway holds it; a plain open of the same file with a record naming the gateway, an unrelated file, fds 0, 2 and -1 are refused; after release, after another holder retakes, and after the gateway retakes (old descriptor), refused; only a new descriptor proves the new lock; a replaced lock file is refused; the check takes no lock when the lock is free.subordinate_lane_in_this_process_needs_the_held_gateway_lock(runtime_control.rs): a record forged as this pid with rolegateway/gateway-publicwhile another holder has the lock is refused ("already owns this identity"); the held gateway lock opens the lane whatever the record says; after release the leftover record is refused; a holder with a non-JSON record is refused; this process holdingserveis refused.subordinate_lane_for_a_home_terminal_needs_its_gateway_lock_descriptor(runtime_control.rs): a record naming the parent as a gateway, with a plain descriptor while another holder has the lock, is refused; the gateway's descriptor opens the lane; a missing or wrong marker, a parent that is not the gateway pid, a missing or non-numeric descriptor are refused; after release and after another holder retakes, refused.active_host_process_reports_a_holder_whose_record_cannot_be_read(host_lock.rs): an empty, non-JSON or wrong-typed record under a held lock is reported as anunknownholder; once released it is free.healthzswitched back to the old JSON check andpublish_gateway_lock_if_unownedswitched back toread_lock_metadata(..).is_some_and(|meta| meta.pid == std::process::id()), the first two tests fail (left: 200 / right: 503,the forged record does not stand in for the lock).held_host_rolerestored to lookup, mutex released, then a fresh-open probe only:a_release_and_retake_...fails (a lock another holder took before the probe was reported as held: Some("gateway")) andan_entry_for_the_same_file_...fails (left: Some("gateway") right: None). (2)subordinate_gateway_hostrestored toactive_host_process+owner.pid == std::process::id()with rolegateway, andparent_gateway_lock_provenrestored to "parent pid equals the record's pid with a gateway role": bothsubordinate_lane_*tests fail (unwrap_err() on an Ok value: true;assertion failed: !proven(&plain, ...)). (3)active_host_processrestored toOk(read_lock_metadata(&lock_path).map(Into::into)):active_host_process_reports_a_holder_...fails (held lock with record "" reported free) and the lane test fails (unwrap_err() on an Ok value: false). With the fixes all pass.docs/LAUNCHER_CONTRACT.mdrestored, both fail on the contract checks (assertion failed: contract.contains("The lock proves possession, not identity."),assertion failed: contract.contains("ELASTOS_GATEWAY_HOST_LOCK_FD")). (2) Withhealthzchanged to also accept "a held lock whose record saysgateway" (treating possession as identity), the/bin/sleeptest fails (a lock held by another processleft: 200 right: 503). Withparent_gateway_lock_provenchanged to also require the record to name the parent asgateway, the child test fails (a descriptor that holds the lock, from a parent that is not a gateway), which shows the test pins exactly what the child's check accepts. Both mutations were reverted.A refused passkey origin returns HTTP 403 with plain text. For a loopback page such as
http://127.0.0.1:8090it says: "Passkeys work on this computer only at the localhost address. Open http://localhost:8090/home/ and try again."The Home origin decision is recorded below under Decision: localhost is the one Home origin.
Readiness gap, stated in
docs/LAUNCHER_CONTRACT.md: the reply comes from whatever process answers on the port and describes that process's own data directory. An ElastOS gateway for another data directory that already listens on the port also returnsOK, and so can any other program on the port. Until a reply names the lock holder, a supervisor confirms that its own child is still running and that the lock records that child's pid. The contract says that record is a hint, not proof: a process that is not that child can hold the lock and write a record that names the child.elastos serveholds the host lock asserve, and its supervisor can start a gateway in the same process (elastos room open). That gateway keeps theservelock and starts, and its/healthzreturns 503 (gateway_inside_serve_starts_and_answers_health_unavailable,api/gateway_server.rs;gateway_inside_a_held_serve_lock_keeps_it,host_lock.rs). It recognises theservelock from the held lock, not from the JSON pid.On this branch the binary watch still exits 75 when the installed binary is replaced, when its path changes, and when the watch cannot read it. A supervisor uses the exit codes. It does not parse English sentences from stdout.
Intel Mac's public release id is
x86_64-darwin. This contract does not publish a download for that id.Head
d30027ed. What this branch carries on top ofdevelop: the launcher contract and localhost work (402d8739..9c9766b3: localhost binds on every loopback address, the 308 move of IP-literal Home pages to localhost, the passkey origin refusal with a plain message), a merge of the zip 9 fix (519a2df9, merging756dad1cfromfix/zip-9-rustsec/ #417) socargo-auditpasses, a cherry-pick of Anders' develop commit50515055asf1fcb219(the model-provider held-lock journal test uses the bounded retry), the lock fixes36720070,85f3142e,eed83a72,4bb19dcf, and the possession wording and tests13cdcad0.d30027ed: merge oforigin/developatbfefe92f(develop's fix for its own #430 Home link test; no conflicts; zip stays 9.0.4). Atd30027ed:cargo test -p elastos-server --lib -- home_external_link possession home_terminal_lane healthz host_lock::27 passed, 1 ignored.developis merged in atbfefe92f(d30027ed). Stack: #395 and #407 are drafts stacked on this branch. #407 is the only branch that changes the exit-75 meanings. Draft againstdevelop. Do not merge.Tested here (Linux worker, no IPv6), head
13cdcad0:cargo test -p elastos-server --lib -- possession subordinate_lane home_terminal_lane healthz host_lock:: gateway_inside_serve28 passed, 1 ignored (the child half of the subprocess test, which the parent test runs), built from this worktree;cargo clippy -p elastos-server --all-targets -- -D warningsclean;cargo fmt --all -- --check,git diff --checkandnode scripts/public-copy-entropy-check.mjspass. At4bb19dcf:-- runtime_control update_controller update:: install_transaction gateway_server home_system home_terminal site_publication content_sandbox frontdoor473 passed, 2 failed for reasons outside this change:readiness_uses_the_reported_ipv6_home_listener_and_checks_its_served_bytes(binds[::1]:0; the worker has no IPv6) andruntime_control::gateway_children::tests::cancelled_start_reaps_group_and_other_gateway_generation_is_preserved(gateway_children.rs:1059!pid_is_alive(pid)), which fails the same way with theelastos-server/srctree of36720070on this host;node scripts/check-product-data.mjsand its test (99 pass) pass. macOS was not run here; the proof usesflockper open file description, a no-op repeatedLOCK_EXby the holder, and device/inode, which macOS has, and the new tests usefork/exec,openandflock, which macOS has. Without device and inode numbers (not a supported Home platform), the proof fails closed.Decision: localhost is the one Home origin
The team chose
localhosteverywhere. Browsers refuse WebAuthn at an IP address (Chromium:SecurityError: This is an invalid domain.athttp://127.0.0.1), so passkeys work only athttp://localhost:<port>. The launcher (Elacity/elastos-launcher #54) changes to open and probehttp://localhost:8090/...; this branch changes the Runtime side.Listeners (
api/gateway_server.rs,bind_gateway_listeners). For--addr localhost:<port>the gateway binds each address the computer resolves forlocalhost, plus127.0.0.1and::1(browsers use both forlocalhostwhatever the hosts file says), deduplicated, on one port, and serves the same router on each with one shared shutdown handle. If another program already uses any of them, or any other bind error occurs, the gateway does not start and exits 1 with, for example,Home cannot start because another program already uses 127.0.0.1:8090. Stop that program, then start Home again.Only a family the computer lacks (EADDRNOTAVAIL,EAFNOSUPPORT, as for::1without IPv6) is left out, and at least one address must bind. Alocalhostthat resolves off this computer is refused. An IP-literal--addrkeeps today's singleTcpListener::bind(addr).host-process.lockstill records--addras given (localhost:8090).Redirect (
home_page_on_localhost). Only when the gateway listens onlocalhost: aGET/HEADfor/,/home,/home/,/apps/home,/apps/home/whoseHostis a loopback IP (127.0.0.1,[::1]) gets 308 to the same path and query athttp://localhost:<bound port>./healthz,/api/..., Home assets and app routes are not redirected, and/healthzanswers 200 on each listener withHost: localhost:<port>(what launcher #54 probes) and with the IP host. A non-loopback Host is not redirected. An IP-literal gateway does not redirect, because it does not own the other localhost family and the redirect could hand the Home origin to another program.Hidden
elastos gatewaydefault stays127.0.0.1:8090. The contract and every script/CI caller pass--addrexplicitly, butelastos gateway --publicuses the default and turns it into a public bind withaddr.replace("127.0.0.1", "0.0.0.0")(gateway_cmd.rs). Changing the default would make--publiclisten on loopback only. The contract says a launcher passes--addr localhost:8090.Docs.
docs/LAUNCHER_CONTRACT.mddrops the single-family gap and states the listeners, the redirect, and thatlocalhostis the one Home origin. It also states what proves that this gateway holds the lock (the heldflock, not the JSON record) and that the lock proves possession, not identity.docs/HOME_LOCAL_SETUP.md(the old "only a script default, not product authority" line),docs/INSTALL.md,docs/MAC.mdanddocs/WINDOWS.md(which namedlocalhost:61180/apps/home/for the Windows launcher) cite the contract.61180is labelled the developer source-home lane.Tests (
api/gateway_server.rs,trusted_gateway_tests):localhost_binds_ipv4_and_ipv6_loopback_on_one_port,localhost_refuses_to_start_when_another_program_holds_ipv4_loopback,localhost_refuses_to_start_when_another_program_holds_ipv6_loopback,localhost_that_points_off_this_computer_is_refused,only_a_missing_address_family_may_be_left_out,localhost_addresses_add_both_browser_loopbacks_once,localhost_port_matches_only_the_name_localhost,ip_literal_addr_binds_one_listener_as_before,ip_literal_gateway_serves_home_pages_in_place,loopback_ip_home_pages_move_to_localhost_and_other_routes_stay. With the old single bind and no redirect, the IPv4 hostile test fails (left: "Address already in use (os error 98)") and the redirect test fails (left: (200, None) right: (308, Some("http://localhost:44553/"))). The worker host has no IPv6 (EAFNOSUPPORT), so the::1parts printSKIPPED IPv6 PARTthere and run only on CI hosts with::1.Remaining gaps. Windows forwarding of
localhostinto WSL2 is not tested here (the contract states this). The::1listener, the::1hostile-process refusal and the::1health and redirect checks did not run on the worker host (no IPv6); they run on CI hosts with::1and are not yet observed passing there. A real-browser passkey ceremony athttp://localhost:8090behind the redirect is still issue 4 below. Removing or replacinghost-process.lockwhile a host runs makes that host's/healthzand subordinate lane refuse, but it still lets a second host take a lock on the new file; that is older than this branch and not fixed here. Any same-user process can hold the lock and so stop Home from starting (the gateway exits 1 withanother ElastOS host already owns); that is the documented boundary, not a fix here. The Home terminal's inherited-descriptor probe has one narrow race: if the gateway releases its lock between the second and third probe step, the third step takes the lock again through the shared open file and the terminal keeps the home locked until it exits; this fails closed (no second host starts) and is documented atinherited_gateway_host_lock_is_held. The Home terminal lane was tested with in-process descriptors and with a real child process given the descriptor by a non-gateway parent, not with a real gateway-spawned PTY child.update_controller.rsprove_ready(fromdevelop) still compares the record'spid,generationandroleas one of several readiness checks (the private attach secret and served bytes are the others); it is not changed here: #437.CI
test-elastoson13cdcad0failed in develop's own Home link test (403 vs 200), because this PR's CI merges develop and develop 8ea4aac/0dbba668 carried that broken test; not this PR's.d30027edmerges develop at bfefe92, which fixes the test. (Run 39090524496, job 117567346067, on13cdcad0.home_external_link_names_the_launched_app_and_the_exact_addresswas ondevelop, not on this branch at13cdcad0; it reached that run only through the CI merge with develop.)source-home-linux (ubuntu-24.04)failed on1e63148f(run 38899842659, job 116995610376). Raw log: thescripts/ci-installed-journeys.sh homestep started at 14:01:42 and printed nothing untilci-installed-journeys.py, line 1261, in run: raise RuntimeError("finite installed UI journey failed")at 14:04:05. The Node journey printed no assertion, so the Python loop stopped it: line 1261 raises whenbrowser_owner.poll() != 0 or finite.error, andfinite.erroris set only by the outside resource observer (observe, lines ~504-508 at that head, which stores the fixed stringfinite_resource_observation_failedand drops the exception text). The uploaded receipt for the 4 GiB journey showsfinite_memory.status: failed, one action (engine_runningat 0.016 s, engine pid 58344),memory_peak_bytes693587968 of 4294967296, allmemory_eventszero (no OOM),cgroup_empty: trueandresults.finite_memory_boundary: failed;finite-model-busy.pngexists and nohome-journey.jsonwas written. So the observer raised after the Busy refusal and before theengine_stoppedstage was recorded. Which of its checks raised is not in any log or artifact (the runtime logjourney-runtime.private.logis not uploaded). One check fits this point and is timing-dependent: onengine_stopped, the observer raisesfinite journey engine group exit is unconfirmedon the first sample where the engine is gone but a member of its process group is still alive (ci-installed-journeys.pylines ~477-479 ondevelop), without waiting; the guard andllama-servershare a process group that Stop kills with oneSIGKILL(capsules/model-provider/src/local_llama.rs:904). That is a hypothesis, not proven. The 1 GiB journey and the engine-absent journey in the same job passed with the same gateway,/healthzprobe and origin. #413 records the observer's phase and message in the receipt, so the next failure names its check.Principles check
localhost, so Home and the credential share one origin. A stored passkey keeps its own host./healthz, the gateway's own lock check and the subordinate runtime lane prove that this gateway (or, for a Home terminal, the parent's descriptor) holds the lock, from theflockheld by its own open file description on the file the path names now, in one step under the registry mutex, not from a record any same-user process can write. The lock proves possession, not identity: any same-user process can hold it, and the contract says so. It does not prove which data directory the supervisor meant; the contract states that gap.localhost, with a loopback-IP Home page redirected to it), and one readiness body.servereports 503 instead of claiming Home readiness. A forged lock record, a foreign holder (also a separate non-ElastOS process), a replaced lock file, a released lock and a retaken lock report 503 and close the subordinate lane; a held lock with an unreadable record counts as held (fail closed).OK, the localhost origin, the lock proof and its possession-not-identity limit, and the readiness gap.x86_64-darwin.Intel stays C until a signed artifact and a journey exist. No grade here is a merge approval.
Team issues to open
This agent cannot open GitHub issues. Please open these:
Signed x86_64-darwin artifact, darwin-amd64 payloads, and Home journey. Body: Keep the signer closed until a producer, adarwin-amd64component set, and an installed Home journey exist. Intel stays hidden until that set lands together.Machine-readable update status for exit 0. Body: Exit 0 currently covers more than one successful update outcome. Add a status a supervisor can read when it must distinguish "up to date" from "a newer release exists".Adversarial review for launcher process control. Body: An independent review from a non-Cursor model family, thenirzhywau, on the exact SHA, for the exit-policy draft and any install or signer change.Browser passkey ceremony at http://localhost:8090. Body: The in-process WebAuthn test registers and signs in at that origin and refuseshttp://127.0.0.1:8090. A browser virtual-authenticator ceremony is still open if that proof is required./healthz reply that a supervisor can bind to its own Home. Body:/healthzanswers for the process on the port. Decide whether the reply carries a value the supervisor can compare with the child it started (for example its pid), and change the contract body if so. The lock file's JSON is not proof of ownership, and the lock itself proves possession, not identity.Prove Windows localhost reaches the WSL2 Home on 127.0.0.1 and ::1. Body: The team chose localhost as the one Home origin and the Runtime now listens on every localhost address. Prove on Windows that the browser'shttp://localhost:8090/home/reaches the WSL2 gateway, and that a Windows process on port 8090 cannot take that origin.