From 3cd2f79f8b33ddf064116d9f36b7ca373afe3df9 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 23 Oct 2026 18:46:59 -0400 Subject: [PATCH 1/6] docs: record v3.5.0's release as published and run its Windows shm tests The v3.5.0 post-release pass. Four documentation fixes and one CI change. Release notes. .github/release-notes/v3.5.0.md merged with placeholder MiSTer lines: the pin, the bitstreams and their verification exist only after the oracle merges, the sibling pins that merge and the bitstreams are compiled from the sibling's release commit. Those three lines were filled in on the published release page; the file now carries the page byte for byte (diffed against `gh release view v3.5.0`, trailing blank lines trimmed). The bitstream line also records the seed-3 pair that was attached for a few hours before the seed-2 rebuild. PGO. Auto Release run 39898198885 built and gated the profile-guided Linux binary and did not ship it ("Ship PGO binary" skipped). The gate's own line: "NO PROMOTE: speedup -4.92% (need > 3.0%) / oracle=true / full build kept=1" -- byte-identical to plain, and 4.92% slower. v3.4.1 measured -5.05%. The CHANGELOG records it, so nobody reads the skipped job as a failure; v2.9.6 stays the only promotion. Windows shared memory. v3.5.0's review fix for growing an OS shared-memory region that another program still holds is Windows-only code with a #[cfg(windows)] test, and no CI job ran it: the test matrix reaches Windows only on a full run, without script-ipc, and every feature-test step is `if: runner.os == 'Linux'`. full-build's Windows leg (every ready PR) now runs `cargo test -p rustynes-frontend --lib --features script-ipc -- shm:: script_host::`. Two traps it handles: `tee` hides cargo's exit status, so the step instead greps for an `ok. N passed` line with N >= 1, which a failure (FAILED), a compile error (no line) and an empty filter (0 passed, exit 0) all miss. On Linux the same command runs 32 tests (1 ignored), so the filter matches; the Windows run is this PR's first, and its result is the evidence. Tickets (to-dos/ROADMAP.md): - T-MMC5-LATCH-VBLANK: the MMC5 split and ExGrafix CHR latches survive VBlank in the oracle (split_chr_bank_latch / ex_chr_bank_latch clear only on the next nametable fetch or a $5104 write) and the MiSTer core matches; the NESdev page suggests hardware may not. Settle from the page and a $2007-in-VBlank stimulus, not from either core. Carries the mmc5.sv detection-comment fix held back at v3.5.0 so the tagged tree matched the compiled one. - T-WIN-SCRIPT-IPC-CI: the step above; closes when it has passed once. - T-V350-REVIEW-RESIDUE: the Antigravity suggestions not taken on #624 and #69, each to be confirmed with a failing test or refuted. docs/agents/mister-cosim.md gains the seed-script trap: the sweep script's footer printed the pre-v2.6.23 rule and the v3.5.0 pin followed it (seed 3 over seed 2) until CodeRabbit caught it on sibling #69. docs/SALVAGE_MANIFEST.md records today's scratch salvage (7,395 files, 166 MB, into the gitignored salvaged/tmp-scratch/f63c2db0/). Verified: markdownlint on every changed Markdown file; pre-commit's YAML and actionlint hooks on ci.yml; release_anchor_audit, release_notes_render_audit, release_state_prose_audit and contribution_checklist_audit pass (45 tests across the four, 0 failed). No code changed, so no clippy or test suites were run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj --- .github/release-notes/v3.5.0.md | 8 ++++---- .github/workflows/ci.yml | 12 ++++++++++++ CHANGELOG.md | 25 +++++++++++++++++++++++++ docs/SALVAGE_MANIFEST.md | 32 ++++++++++++++++++++++++++++++++ docs/agents/mister-cosim.md | 1 + to-dos/ROADMAP.md | 3 +++ 6 files changed, 77 insertions(+), 4 deletions(-) diff --git a/.github/release-notes/v3.5.0.md b/.github/release-notes/v3.5.0.md index 6d02e0ea..405a1f98 100644 --- a/.github/release-notes/v3.5.0.md +++ b/.github/release-notes/v3.5.0.md @@ -51,20 +51,20 @@ v3.5.0 is a creator-tools release. Movies record the Zapper and the other expans - **The Famicom peripherals**: the Family BASIC keyboard from a PS/2 keyboard, the Power Pad on port 2, and the second controller's microphone, gated against the NESdev pages. - **Provenance disclosures** (the sibling's `docs/provenance.md`, "Disclosures"): three reads that touched derived regions of the oracle while this release was written, none of them used: the oracle's Power Pad region, read before it carried its header (the RTL follows the NESdev page and is gated against it); the comment above the oracle's EEPROM model (the data sheets were followed); and the Namco 163's output level, seen in a search result (the RTL's level comes from the NESdev page's measurements, and the audio comparison is blind to a uniform scale). They are recorded for outside review, not certified clean. - **The MMC5 detector was checked by mutation**: six deliberate defects, three caught at once; one (the 241st-line exit) is unreachable, since line 240 makes no reads, and the other two were caught after a new stimulus that reads the IRQ status by hand (`mapper5irqack406`). -- **The oracle pin moves to v3.5.0**: the sibling pins this release's merge and re-checks every golden against it before its own release; the counts are in its v3.5.0 notes. +- **The oracle pin moves to v3.5.0** (this release's merge, `5d441f44`): `VERIFY=1` reproduces all 256 stems and 1,156 artifacts byte for byte. - **The co-simulation ladder**: on the die 315 passed, 0 failed, 4 expected failures; off the die 365 passed, 0 failed, 10 expected failures. The four on both are `chrram-fetch` (18 of 185,725 fetches), *Mississippi Satsujin Jiken* frame 140, *Micro Machines* frame 71 and the Sunsoft 5B timed gate; of the six off the die only, five are where the oracle departs from the NESdev pages or the EEPROM data sheets (two Bandai EEPROM stimuli, the VRC7's WRAM enable, and the VRC6 and MMC5 audio timing), each graded both ways, and one is v3.4.0's *Ganbare Goemon Gaiden* frame 25, eight pixels lost to the SDRAM's CHR latency. -- **New bitstreams** are compiled from the sibling's release commit after this release merges and attached here and to the sibling's release; their seeds, checksums and timing margins are in its v3.5.0 notes. +- **New bitstreams** at seed 2, BUILD_DATE 2026-10-23, each byte-identical across two clean compiles: the off-die `RustyNES_MiSTer-v3.5.0.rbf` (md5 `737048b65de5c712cc5d40fc26b906fa`, setup +0.380 / hold +0.081 ns, SDRAM read capture +0.436 / +1.176 ns over all four corners) and the on-die `RustyNES_MiSTer-v3.5.0-lite.rbf` (md5 `3db762d73bdc3d685638121cd0ee360b`, setup +0.231 / hold +0.074 ns). They replace a seed-3 pair attached for a few hours on 2026-10-23: seed 3 followed an outdated reading of the seed rule (found in the MiSTer release review), and the rebuild also carries that review's fixes, among them two Family BASIC keys held across their twin's release and the off-die OSD's ROM-size message. ## Verification - `cargo test --workspace --features test-roms,commercial-roms --no-fail-fast`: 3,920 passed, 0 failed, 19 ignored, with the commercial suites at 60/0, 169/0 and 6/0. - `cargo fmt --check`; `cargo clippy --workspace --all-targets -D warnings`, and again with `scripting`, `scripting,hd-pack`, `retroachievements` and `full`, for both browser builds, and for the excluded `rustynes-cosim`; `cargo doc` with `-D warnings`; the `thumbv7em` `no_std` build; markdownlint. All clean. -- MiSTer: the co-simulation ladders above; the pin re-verification and the two-compile bitstream checks are recorded in the sibling's v3.5.0 notes. +- MiSTer: the co-simulation ladders above; `VERIFY=1` at `5d441f44` (256 stems, 1,156 artifacts); `release-rbf.sh v3.5.0 --no-build` passed every check against the compile date; both release assets downloaded back match the md5 above. ## Install - Download the pre-built binaries for Linux, macOS, and Windows below. -- The MiSTer bitstreams are attached below once they are compiled from the sibling's release commit, as a release candidate, not hardware-verified: `RustyNES_MiSTer-v3.5.0.rbf` (off-die, needs SDRAM) and `RustyNES_MiSTer-v3.5.0-lite.rbf` (on-die). +- The MiSTer bitstreams are attached below as a release candidate, not hardware-verified: `RustyNES_MiSTer-v3.5.0.rbf` (off-die, needs SDRAM) and `RustyNES_MiSTer-v3.5.0-lite.rbf` (on-die). - The WebAssembly build is live at [doublegate.github.io/RustyNES](https://doublegate.github.io/RustyNES/). - The RetroArch core is in RetroArch's Online Updater on the platforms the libretro buildbot publishes to. - Licensed under GPL-3.0-or-later. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad26701e..cd12078f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -614,6 +614,18 @@ jobs: with: cache-key: full-build - run: cargo build -p rustynes-frontend --features full --locked + # 2026-10-23 (after v3.5.0) — the OS shared-memory store (`shm.rs`, `script-ipc`) has a + # Windows implementation and `#[cfg(windows)]` tests, among them v3.5.0's + # grow-while-another-program-holds-it recovery, and no job ran them: the + # `test` matrix runs Windows only on a full run and without `script-ipc`, + # and the feature steps above are Linux only. This runs the store's tests + # and the script host's on Windows, on every ready PR. The name filter + # must match something: an empty filter exits 0. + - if: runner.os == 'Windows' + shell: bash + run: | + cargo test -p rustynes-frontend --lib --features script-ipc --locked -- shm:: script_host:: 2>&1 | tee test.log + grep -E '^test result: ok\. [1-9][0-9]* passed' test.log test-roms: name: test (test-roms feature) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f39a444..8737b987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,31 @@ cycle-accurate core later replaced. ## [Unreleased] +### Changed + +- **CI runs the shared-memory store's Windows tests**: the Lua IPC transports' + OS shared-memory code has a Windows implementation, and v3.5.0 added a + Windows test for growing a region another program still holds, but no CI job + built `script-ipc` on Windows. The `full-build` job's Windows leg now runs the + store's and the script host's tests with it, and fails if the filter matches + no test (`T-WIN-SCRIPT-IPC-CI`). + +### Documentation + +- **v3.5.0's release notes file matches the published release**: the MiSTer + pin, bitstream and verification lines were placeholders in the merged file + and were filled in on the release page once the sibling had pinned the merge + and compiled the bitstreams; the file now carries the published text. +- **v3.5.0's Linux download is the plain build**: the release's PGO gate did + not promote. The profile-guided build was byte-identical to the plain one on + the oracle check and measured 4.92% slower (the gate needs more than 3% + faster), as v3.4.1's measured 5.05% slower. The only promotion in the release + history is still v2.9.6's. +- New tickets from the v3.5.0 reviews: `T-MMC5-LATCH-VBLANK` (the MMC5 split + and ExGrafix CHR latches survive VBlank in both cores; a documentation and + test-ROM question first), `T-WIN-SCRIPT-IPC-CI` and `T-V350-REVIEW-RESIDUE` + (suggestions not taken, to be confirmed or refuted). + ## [3.5.0] - 2026-10-22 - "Chorus" (creator tools: the full movie input stream, Lua WebSockets and shared memory, an MCP server, speedrun timing and dump checks; and the MiSTer core's MMC5, VRC6, Namco 163, VRC7 and Bandai FCG boards, off the die) ### Breaking diff --git a/docs/SALVAGE_MANIFEST.md b/docs/SALVAGE_MANIFEST.md index 22101580..36cce7b4 100644 --- a/docs/SALVAGE_MANIFEST.md +++ b/docs/SALVAGE_MANIFEST.md @@ -364,3 +364,35 @@ ladder worktree copies; `live/turn1.json`, a short-lived TURN credential that must not be kept. Earlier the same evening, 10 GB of this session's regenerable build output (a cargo target directory and Verilator `obj_dir` trees) was deleted to recover a full `/tmp`. + +## 2026-10-23 — session f63c2db0, after the v3.5.0 release + +Scope set by the maintainer: this session's scratch, curated tier. The dry run +found 60,331 files (4.9 GB) in the session scratchpad. The cut was made by +kind, not by name: build output (cargo `target` / `base-target` trees, `.o`, +`.dwo`, `.bc`, `.rmeta`, Verilator `obj_dir`, wasm `dist-*`), the CodeQL +bundle, databases and repository copies (2.5 GB), downloaded release binaries +and `.rbf` copies (on the GitHub releases and in `salvaged/evidence/v3.5.0-quartus/`), +and any PNG, snapshot, CSV or binary over 256 KB, which takes the golden IRQ +CSVs (`VERIFY=1` rebuilds them from the pinned oracle) and the 137 MB DUT dump. +**Moved: 7,395 files (166 MB)** into `salvaged/tmp-scratch/f63c2db0/`, relative +paths kept, no destination existed, no errors: + +- `v350/` (7,267): the v3.5.0 agent and merge work. Oracle worktree scratch + (`ora-mmc5irq`, `ora-desktop`, `ora-script`, `ora-movie`, `ora-mcp`, + `ora-small`), sibling board work (`sib-mmc5`, `sib-namco-vrc6`, + `sib-fcg-vrc7`, `sib-tier`, with the generated stimulus `.nes` files), + `m5det` / `m5merge` / `mmc5eq` (the MMC5 detector change and merge), + the mutation runs, gate logs, review replies for #624 and #69, the seed-sweep + logs in `quartus/`, and the cut, review and PR-body drafts. +- loose files (90) and `mister/`, `v341/`, `cfdocs/`: v3.4.1 and v3.4.2 notes, + merge logs, CI workflow experiments, and a Worker tail record (no credential). + +Checked for credentials before writing this entry: the only hits are repository +copies of the hosted service's docs and tests, Cloudflare's own credential +documentation, and the tail record above. + +**Left in /tmp:** 53,776 files (5.1 GB) of the kinds above; the 181 +background-task output logs; the VeridianOS agent slug (37 files) and the +`rusty2600-target`, `spectre-target` and `octollm-target` build trees, all of +which belong to other projects; the zellij log and harness cache-state JSON. diff --git a/docs/agents/mister-cosim.md b/docs/agents/mister-cosim.md index 1ba6f598..060d25a4 100644 --- a/docs/agents/mister-cosim.md +++ b/docs/agents/mister-cosim.md @@ -40,6 +40,7 @@ - **THE ELEVEN-VARIANT SWEEP CLOSED (v2.6.23): `chrram-live` GREEN, 32,861 -> 0 PIXELS.** The axis the sweep missed (recorded above) was what a `$2007` access during rendering COMPUTES FROM, not what it computes. On silicon the access pulses the rendering pipeline's existing load rather than performing its own increment. Three changes, each exposing the next: `v_pipeline` (increment the value *this dot's* pipeline load produced, 21,941 -> 2,646 diverging fetches), `v_acc_addr` (RESOLVE the access against that same value, -> 18, which stopped 33 nametable cells being written one coarse-X cell early), and `chr_wr_addr` sourced from it too (-> 0 pixels). `chr_wr` asserts 4,389 times before and after — same stimulus. The `chr_wr_addr` step is a MAINTAINER DECISION (ADR 0037 territory: the write address the oracle now follows is a case the wiki declares undefined), taken knowingly and kept in the RTL comment marked resolved-by-decision rather than deleted, so it governs the next case like it. `chrram-fetch` stays `run_xfail` at 18 of 185,725 — all garbage sprite-window nametable fetches (dots 257-320) whose data is discarded, which is why the picture is byte-perfect while the trace is not. - **GOLDEN RECIPES: 97 OF 110 MANIFESTS, GENERATED NOT TRANSCRIBED, AND 472 ARTIFACTS BYTE-FOR-BYTE (v2.6.23).** `tb/gen_golden_recipes.py` derives each recipe row from fields the manifest already carries (`frames_req`, `ckpt_interval`, `obs_count` sizing `--irq-trace`, `press_start`), because 110 hand-transcribed caps is 110 silent ways to under-cover a golden. `VERIFY=1 tb/fetch-goldens.sh` regenerates into scratch and diffs against `goldens/` WITHOUT writing to it — **472 artifacts byte-for-byte** across goldens recorded as rustynes 2.5.1 through 2.6.22, fifteen releases. **Every apparent difference across four diff passes was a recipe defect, never oracle drift**: a `.irq.csv` diagnostic nothing reads; a guessed `--apu-trace` cap the exporter correctly REFUSED rather than silently under-covering; eleven blargg stems producing nothing because the exporter names artifacts after the ROM FILE's stem, not the target stem; and AccuracyCoin differing in 11.4% of its framebuffer with a MATCHING rom hash — actually a missing `--press-start`, caught by `accuracycoin_status` refusing to score a vacuous run ("every one of the 144 SCORED entries is NotRun"). The ROM is located by `rom_sha256` (content), never the manifest's unusable `rom` path (relative / `../../../`-relative / absolute-under-$HOME / pointing into deleted `/tmp/blargg-apu/` depending on the stem). - **FITTER SEED 4 PINNED ON BEST-OVERALL, NOT STRICT BINDING-MARGIN (v2.6.23).** RTL changed, so by `RustyNES.qsf`'s own rule the prior sweep described a design that no longer exists. Re-swept, 5 seeds, all closing; hold binds at every seed (0.078-0.115ns vs setup's 0.247-0.421ns), so the established rule mechanically selects seed 1 (+0.115 hold). Maintainer decision pinned seed 4 instead (+0.112 hold, +0.421 setup) on BEST OVERALL — within 3ps of seed 1 on hold while beating it by 174ps on setup — with the reasoning AND the falsifying experiment (re-fit one seed repeatedly, measure run-to-run variance; not yet done) written into the `.qsf` alongside the pinned seed, not left implicit. Marked `<- pinned`, not `<- published`, since no bitstream had been built from it yet at write time. Seed 5 — the PREVIOUS release's outright winner — is joint-worst on this RTL: a sweep result belongs to one specific RTL, never carries forward. +- **THE SEED RULE IS THE ONE IN THE `.qsf`, NOT THE ONE A SCRIPT PRINTS (v3.5.0).** `scripts/seed-sweep.sh`'s footer still printed the pre-v2.6.23 rule ("maximise the BINDING margin"), and the v3.5.0 pin followed the printout: seed 3, 4 ps ahead of seed 2 on on-die hold, over seed 2's 132 ps more on-die setup and its far better headline off-die result (+0.224 / +0.088 against +0.027 / +0.081). CodeRabbit caught it on sibling #69, after the seed-3 pair had been on the public oracle release for about four hours; both releases were rebuilt and re-uploaded at seed 2. The script now prints the recorded criterion (sibling `800b1d6`). **Read the criterion from the qsf's v2.6.23 entry**, and treat any tool that restates a rule as a copy that can go stale. - **AN AUDIT'S "THE RTL DISAGREES WITH THE ORACLE" DOES NOT SAY WHICH SIDE IS WRONG (v2.8.2).** RTL audit R-3.5a said `cart.sv` checked MMC1's reset bit before the consecutive-write filter "while the oracle filters first", and called the RTL the defect. nesdev MMC1 says a reset is never ignored and only a DATA write on the cycle after a write is, so the ORACLE was wrong (`m001_mmc1.rs`, fixed red-first in v2.8.2) and the RTL was left alone. The v2.7.0 pulse-1 sweep finding had the same shape. Before fixing either side of a disagreement, read the wiki for the rule itself; the audit ledger now has an INVERTED verdict for this case (`docs/audits/README.md`). - **A DRIFTING LOOP REACHES A DOT ONLY IF ITS PERIOD IS COPRIME WITH THE FRAME (v2.8.2).** The first `ppulatch072` draft, an 11-cycle `$2002`/`$2000` loop, is 33 dots, and 11 divides the 89,342-dot NTSC frame (2·11·31·131), so the loop reaches only 3 of 33 landing residues and could miss the VBL set dot forever. A `NOP` makes it 13 cycles (39 dots, coprime), and 39 frames cover every dot. Coverage was then read from the oracle's `irq.csv` (reads at scanline 241 dots 0-4, dot 1 twice), not assumed. The reverse case is `apureload042`'s: a walk resolves a residue class, not a cycle, so a one-cycle target inside a long period needs a PLACED write instead. - **`release-rbf.sh`'S STALENESS CHECK READS MTIMES, AND A BRANCH SWITCH REWRITES THEM (v2.8.2).** After the sibling's v2.8.2 PR merged, `git switch main && git pull` touched `rtl/apu2a03.sv`, `rtl/cart/cart.sv` and `rtl/ppu2c02.sv`, and `scripts/release-rbf.sh v2.8.2 --no-build` then refused: "RustyNES.sta.rpt is OLDER than 3 source file(s)". The content was identical -- `git diff b09ee32 927ef4a -- rtl/` (the compiled commit against the merge) was 0 lines -- so the report matched the tree and only the timestamps moved. The assets were uploaded with the script's own fallback command, `gh release upload v2.8.2 releases/.rbf --clobber`. The check is right to be conservative; the lesson is ORDER: stage and upload from the PR branch before switching, or prove content identity against the WORKING TREE before overriding it, never by argument: `git diff -- rtl/` (commit against working tree, so staged and unstaged edits count) must be empty AND `git status --porcelain -- rtl/` must print nothing (an untracked RTL file is an input too). `git diff HEAD` compares two commits and would miss an uncommitted edit (CodeRabbit on #559). diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md index e0ee106a..21f0c67a 100644 --- a/to-dos/ROADMAP.md +++ b/to-dos/ROADMAP.md @@ -1476,6 +1476,9 @@ re-minted: `T-PS-dual-runahead` (ADR 0032; **DONE v3.1.0**), `T-MISTER-SAVESTATE | `T-RA-HARDCORE-RESUME` | Android's auto-resume of the on-background state goes through `load_state`, so in hardcore it is refused and the game starts from power-on; RA asks a resumed session to drop to casual instead. A maintainer decision: keep the refusal, or resume and drop to casual | v3.4.0's audit | a maintainer decision | | `T-NETPLAY-MESH-NATIVE` | Native 3-4 player netplay over `mesh_net.rs`, then mobile. **Desktop DONE v3.4.0** (direct IP and LAN, over the authenticated mesh with the Four Score); mobile (MOB-09) follows | NET-01, MOB-09 | v3.4.0 | | `T-MOVIE-INPUT-STREAM` | The Zapper, microphone, Vs. coins and FDS events in `.rnm`. **DONE v3.5.0** (`.rnm` format 7, `rustynes_core::ExtraInput`; every expansion device and Datach scans too; netplay unchanged) | CR-03 | v3.5.0 | +| `T-MMC5-LATCH-VBLANK` | The MMC5's split-screen and ExGrafix CHR bank latches survive VBlank (found in the v3.5.0 sibling review). In the oracle, `split_chr_bank_latch` and `ex_chr_bank_latch` are cleared only by the next nametable fetch or a `$5104` write, and the MiSTer core matches it, so a `$2007` CHR read during VBlank uses the last rendered tile's bank. The NESdev MMC5 page suggests hardware may not; settle it from the page and a test ROM, not from either core. Gate: a stimulus that reads CHR through `$2007` in VBlank after a split or ExGrafix scanline, with the expected bank stated from documentation, passing in both cores. The same change corrects `mmc5.sv`'s scanline-detection comment ("the read after three matching nametable reads"), left alone at v3.5.0 so the tagged tree matched the compiled one | (#69 review) | v3.5.1 | +| `T-WIN-SCRIPT-IPC-CI` | The OS shared-memory store's Windows code ran in no CI job (found v3.5.0): the `test` matrix reaches Windows only on a full run and without `script-ipc`. **Wired 2026-10-23** (the v3.5.0 post-release PR): `full-build`'s Windows leg runs the `shm::` and `script_host::` tests with `script-ipc` and refuses an empty filter. Closes when that step has passed once, including v3.5.0's grow-while-held recovery test | (#624 review) | v3.5.1 | +| `T-V350-REVIEW-RESIDUE` | Antigravity suggestions on #624 and #69 not taken at v3.5.0, each to be confirmed or refuted against the code: a device key's latched state across a rebind; `lua_eval` from the MCP server while a real-time run is going; reads of user-chosen ROM, patch and splits paths with no size bound; and in the sibling, the dead `x_hit_q` register and the `nt_ovr` / IRQ outputs not gated by the board select. Gate: each item closed with a failing test and its fix, or with the reason it does not apply | (#624, #69 review) | v3.5.1 | | `T-LIBRETRO-OPTIONS-V2` | Core Options v2 and float-audio negotiation, behind capability checks | ecosystem 3 | v3.6.0 | | `T-LIBRETRO-CONSOLES` | Switch, PS Vita and 3DS investigated for the libretro core (D28) | LR-04 | v3.6.0 | | `T-ANDROID-API37` | `ACCESS_LOCAL_NETWORK` for LAN netplay, lifecycle-safe background audio, `targetSdk` 37 | ecosystem 5 | v3.6.0 | From 0a05009b8caea07ccd50605c2151bdcb32541dc2 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 23 Oct 2026 19:07:13 -0400 Subject: [PATCH 2/6] ci: require each Windows shm test group to pass, on the full build Review of #625 (CodeRabbit and Antigravity, the same finding): libtest ORs its name filters into one run with one summary line, so the step's check, a non-zero `ok. N passed`, held as long as ANY of `shm::` and `script_host::` matched. A rename or a #[cfg] change emptying one group would have left the step green with that group untested, which is the exact silent-coverage failure the step was added to end. The check is now per group: each of shm::, script_host:: and memif:: must show at least one `test ::... ... ok` line, and the Windows-only test the step exists for, script_host::tests::a_larger_write_with_a_reader_attached_is_refused_and_the_region_kept, must pass by name. `set -o pipefail` makes a cargo failure fail the step directly instead of relying on the grep. Antigravity's second point: the step built with `--features script-ipc` right after the job built `--features full`, so Cargo compiled the dependency chain a second time under another feature fingerprint and cached both. It now tests with `full`, which contains script-ipc and memif, reusing the build and adding the memif:: tests to the run. Verified on Linux with the exact command: 35 passed, 1 ignored; per-group pattern counts shm 9, script_host 23, memif 3. The named test is #[cfg(windows)], so on Linux only its twin (a_larger_write_replaces_the_region) runs; the Windows leg of this PR is the first execution of the named one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj --- .github/workflows/ci.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd12078f..825d616b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -619,13 +619,25 @@ jobs: # grow-while-another-program-holds-it recovery, and no job ran them: the # `test` matrix runs Windows only on a full run and without `script-ipc`, # and the feature steps above are Linux only. This runs the store's tests - # and the script host's on Windows, on every ready PR. The name filter - # must match something: an empty filter exits 0. + # and the script host's on Windows, on every ready PR. + # + # `full`, not `script-ipc` alone: it is the feature set the step above + # just built, so the dependencies are reused rather than rebuilt under a + # second fingerprint, and it adds the `memif::` tests (Antigravity, #625). + # The checks after it exist because a passing summary proves too little: + # an empty filter exits 0, and libtest ORs its filters, so one group can + # carry the summary while another matches nothing (CodeRabbit and + # Antigravity, #625). So each group must show a passing test, and the + # Windows-only grow-recovery test must be among them by name. - if: runner.os == 'Windows' shell: bash run: | - cargo test -p rustynes-frontend --lib --features script-ipc --locked -- shm:: script_host:: 2>&1 | tee test.log - grep -E '^test result: ok\. [1-9][0-9]* passed' test.log + set -o pipefail + cargo test -p rustynes-frontend --lib --features full --locked -- shm:: script_host:: memif:: 2>&1 | tee test.log + for group in shm script_host memif; do + grep -qE "^test ${group}::[^ ]+ \.\.\. ok$" test.log || { echo "no passing ${group}:: test ran"; exit 1; } + done + grep -qE '^test script_host::tests::a_larger_write_with_a_reader_attached_is_refused_and_the_region_kept \.\.\. ok$' test.log test-roms: name: test (test-roms feature) From 7f1682d638d229a69eb193c658432e33ca7a51cd Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 23 Oct 2026 19:11:58 -0400 Subject: [PATCH 3/6] ci: run the shm tests on macOS too; correct the step's description Antigravity's second pass on #625, three points, all taken. macOS. shm.rs has an Apple implementation of its own (target_vendor = "apple": the variadic shm_open, PSHMNAMLEN's 31-byte name limit, a u16 st_mode, a single-resize ftruncate), and full-build compiles it on macos-15 exactly as it compiles the Windows one, yet only the Windows leg ran the tests. The step now runs on both legs. The per-group checks (shm::, script_host::, memif:: each with a passing test) apply to both; the by-name check for the Windows-only grow-recovery test is gated on RUNNER_OS, since on macOS its #[cfg(not(windows))] twin runs instead. Descriptions. The CHANGELOG said no job built script-ipc on Windows, which is wrong: full-build has compiled `full` (which includes it) on Windows and macOS since v2.9.7; what no job did was run its tests. That entry and the T-WIN-SCRIPT-IPC-CI row also still described the first version of the step (script-ipc, two groups). Both now say what the step does, and the ticket closes when it has passed on both systems. Margins. docs/agents/mister-cosim.md quoted seed 2's off-die +0.224 / +0.088 beside release notes giving +0.380 / +0.081 for the same seed. Both are right for different builds: the first is the 2026-10-22 sweep, before the review fixes changed the RTL; the second is the released rebuild of 2026-10-23. The bullet now says which is which. Verified: pre-commit (check-yaml, actionlint, markdownlint) on the four files; release_anchor_audit 18/0 and release_state_prose_audit 9/0. The macOS and Windows runs of this PR are the first executions of those platforms' shm tests. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj --- .github/workflows/ci.yml | 22 +++++++++++++--------- CHANGELOG.md | 13 +++++++------ docs/agents/mister-cosim.md | 2 +- to-dos/ROADMAP.md | 2 +- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 825d616b..df55d37d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -614,12 +614,15 @@ jobs: with: cache-key: full-build - run: cargo build -p rustynes-frontend --features full --locked - # 2026-10-23 (after v3.5.0) — the OS shared-memory store (`shm.rs`, `script-ipc`) has a - # Windows implementation and `#[cfg(windows)]` tests, among them v3.5.0's - # grow-while-another-program-holds-it recovery, and no job ran them: the - # `test` matrix runs Windows only on a full run and without `script-ipc`, - # and the feature steps above are Linux only. This runs the store's tests - # and the script host's on Windows, on every ready PR. + # 2026-10-23 (after v3.5.0) — the OS shared-memory store (`shm.rs`, + # `script-ipc`) has a Windows implementation and an Apple one + # (`target_vendor = "apple"`: variadic `shm_open`, `PSHMNAMLEN`, a `u16` + # `st_mode`), each with its own tests, among them v3.5.0's Windows + # grow-while-another-program-holds-it recovery. This job built them and no + # job ran them: the `test` matrix reaches Windows and macOS only on a full + # run and without `script-ipc`, and the feature steps above are Linux only. + # This runs the store's, the script host's and memif's tests on both, on + # every ready PR (macOS: Antigravity, #625). # # `full`, not `script-ipc` alone: it is the feature set the step above # just built, so the dependencies are reused rather than rebuilt under a @@ -629,15 +632,16 @@ jobs: # carry the summary while another matches nothing (CodeRabbit and # Antigravity, #625). So each group must show a passing test, and the # Windows-only grow-recovery test must be among them by name. - - if: runner.os == 'Windows' - shell: bash + - shell: bash run: | set -o pipefail cargo test -p rustynes-frontend --lib --features full --locked -- shm:: script_host:: memif:: 2>&1 | tee test.log for group in shm script_host memif; do grep -qE "^test ${group}::[^ ]+ \.\.\. ok$" test.log || { echo "no passing ${group}:: test ran"; exit 1; } done - grep -qE '^test script_host::tests::a_larger_write_with_a_reader_attached_is_refused_and_the_region_kept \.\.\. ok$' test.log + if [ "$RUNNER_OS" = Windows ]; then + grep -qE '^test script_host::tests::a_larger_write_with_a_reader_attached_is_refused_and_the_region_kept \.\.\. ok$' test.log + fi test-roms: name: test (test-roms feature) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8737b987..4f70c7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,12 +28,13 @@ cycle-accurate core later replaced. ### Changed -- **CI runs the shared-memory store's Windows tests**: the Lua IPC transports' - OS shared-memory code has a Windows implementation, and v3.5.0 added a - Windows test for growing a region another program still holds, but no CI job - built `script-ipc` on Windows. The `full-build` job's Windows leg now runs the - store's and the script host's tests with it, and fails if the filter matches - no test (`T-WIN-SCRIPT-IPC-CI`). +- **CI runs the shared-memory store's Windows and macOS tests**: the Lua IPC + transports' OS shared-memory code has Windows and Apple implementations, and + v3.5.0 added a Windows test for growing a region another program still + holds. The `full-build` job compiled that code on both systems and no job ran + its tests. Both legs now run the store's, the script host's and `memif`'s + tests on the `full` build; each group must show a passing test, and on + Windows the grow test must pass by name (`T-WIN-SCRIPT-IPC-CI`). ### Documentation diff --git a/docs/agents/mister-cosim.md b/docs/agents/mister-cosim.md index 060d25a4..83288b2e 100644 --- a/docs/agents/mister-cosim.md +++ b/docs/agents/mister-cosim.md @@ -40,7 +40,7 @@ - **THE ELEVEN-VARIANT SWEEP CLOSED (v2.6.23): `chrram-live` GREEN, 32,861 -> 0 PIXELS.** The axis the sweep missed (recorded above) was what a `$2007` access during rendering COMPUTES FROM, not what it computes. On silicon the access pulses the rendering pipeline's existing load rather than performing its own increment. Three changes, each exposing the next: `v_pipeline` (increment the value *this dot's* pipeline load produced, 21,941 -> 2,646 diverging fetches), `v_acc_addr` (RESOLVE the access against that same value, -> 18, which stopped 33 nametable cells being written one coarse-X cell early), and `chr_wr_addr` sourced from it too (-> 0 pixels). `chr_wr` asserts 4,389 times before and after — same stimulus. The `chr_wr_addr` step is a MAINTAINER DECISION (ADR 0037 territory: the write address the oracle now follows is a case the wiki declares undefined), taken knowingly and kept in the RTL comment marked resolved-by-decision rather than deleted, so it governs the next case like it. `chrram-fetch` stays `run_xfail` at 18 of 185,725 — all garbage sprite-window nametable fetches (dots 257-320) whose data is discarded, which is why the picture is byte-perfect while the trace is not. - **GOLDEN RECIPES: 97 OF 110 MANIFESTS, GENERATED NOT TRANSCRIBED, AND 472 ARTIFACTS BYTE-FOR-BYTE (v2.6.23).** `tb/gen_golden_recipes.py` derives each recipe row from fields the manifest already carries (`frames_req`, `ckpt_interval`, `obs_count` sizing `--irq-trace`, `press_start`), because 110 hand-transcribed caps is 110 silent ways to under-cover a golden. `VERIFY=1 tb/fetch-goldens.sh` regenerates into scratch and diffs against `goldens/` WITHOUT writing to it — **472 artifacts byte-for-byte** across goldens recorded as rustynes 2.5.1 through 2.6.22, fifteen releases. **Every apparent difference across four diff passes was a recipe defect, never oracle drift**: a `.irq.csv` diagnostic nothing reads; a guessed `--apu-trace` cap the exporter correctly REFUSED rather than silently under-covering; eleven blargg stems producing nothing because the exporter names artifacts after the ROM FILE's stem, not the target stem; and AccuracyCoin differing in 11.4% of its framebuffer with a MATCHING rom hash — actually a missing `--press-start`, caught by `accuracycoin_status` refusing to score a vacuous run ("every one of the 144 SCORED entries is NotRun"). The ROM is located by `rom_sha256` (content), never the manifest's unusable `rom` path (relative / `../../../`-relative / absolute-under-$HOME / pointing into deleted `/tmp/blargg-apu/` depending on the stem). - **FITTER SEED 4 PINNED ON BEST-OVERALL, NOT STRICT BINDING-MARGIN (v2.6.23).** RTL changed, so by `RustyNES.qsf`'s own rule the prior sweep described a design that no longer exists. Re-swept, 5 seeds, all closing; hold binds at every seed (0.078-0.115ns vs setup's 0.247-0.421ns), so the established rule mechanically selects seed 1 (+0.115 hold). Maintainer decision pinned seed 4 instead (+0.112 hold, +0.421 setup) on BEST OVERALL — within 3ps of seed 1 on hold while beating it by 174ps on setup — with the reasoning AND the falsifying experiment (re-fit one seed repeatedly, measure run-to-run variance; not yet done) written into the `.qsf` alongside the pinned seed, not left implicit. Marked `<- pinned`, not `<- published`, since no bitstream had been built from it yet at write time. Seed 5 — the PREVIOUS release's outright winner — is joint-worst on this RTL: a sweep result belongs to one specific RTL, never carries forward. -- **THE SEED RULE IS THE ONE IN THE `.qsf`, NOT THE ONE A SCRIPT PRINTS (v3.5.0).** `scripts/seed-sweep.sh`'s footer still printed the pre-v2.6.23 rule ("maximise the BINDING margin"), and the v3.5.0 pin followed the printout: seed 3, 4 ps ahead of seed 2 on on-die hold, over seed 2's 132 ps more on-die setup and its far better headline off-die result (+0.224 / +0.088 against +0.027 / +0.081). CodeRabbit caught it on sibling #69, after the seed-3 pair had been on the public oracle release for about four hours; both releases were rebuilt and re-uploaded at seed 2. The script now prints the recorded criterion (sibling `800b1d6`). **Read the criterion from the qsf's v2.6.23 entry**, and treat any tool that restates a rule as a copy that can go stale. +- **THE SEED RULE IS THE ONE IN THE `.qsf`, NOT THE ONE A SCRIPT PRINTS (v3.5.0).** `scripts/seed-sweep.sh`'s footer still printed the pre-v2.6.23 rule ("maximise the BINDING margin"), and the v3.5.0 pin followed the printout: seed 3, 4 ps ahead of seed 2 on on-die hold, over seed 2's 132 ps more on-die setup and its far better headline off-die result (+0.224 / +0.088 against +0.027 / +0.081 in the 2026-10-22 sweep, before the review fixes; the released seed-2 rebuild of 2026-10-23 measured +0.380 / +0.081). CodeRabbit caught it on sibling #69, after the seed-3 pair had been on the public oracle release for about four hours; both releases were rebuilt and re-uploaded at seed 2. The script now prints the recorded criterion (sibling `800b1d6`). **Read the criterion from the qsf's v2.6.23 entry**, and treat any tool that restates a rule as a copy that can go stale. - **AN AUDIT'S "THE RTL DISAGREES WITH THE ORACLE" DOES NOT SAY WHICH SIDE IS WRONG (v2.8.2).** RTL audit R-3.5a said `cart.sv` checked MMC1's reset bit before the consecutive-write filter "while the oracle filters first", and called the RTL the defect. nesdev MMC1 says a reset is never ignored and only a DATA write on the cycle after a write is, so the ORACLE was wrong (`m001_mmc1.rs`, fixed red-first in v2.8.2) and the RTL was left alone. The v2.7.0 pulse-1 sweep finding had the same shape. Before fixing either side of a disagreement, read the wiki for the rule itself; the audit ledger now has an INVERTED verdict for this case (`docs/audits/README.md`). - **A DRIFTING LOOP REACHES A DOT ONLY IF ITS PERIOD IS COPRIME WITH THE FRAME (v2.8.2).** The first `ppulatch072` draft, an 11-cycle `$2002`/`$2000` loop, is 33 dots, and 11 divides the 89,342-dot NTSC frame (2·11·31·131), so the loop reaches only 3 of 33 landing residues and could miss the VBL set dot forever. A `NOP` makes it 13 cycles (39 dots, coprime), and 39 frames cover every dot. Coverage was then read from the oracle's `irq.csv` (reads at scanline 241 dots 0-4, dot 1 twice), not assumed. The reverse case is `apureload042`'s: a walk resolves a residue class, not a cycle, so a one-cycle target inside a long period needs a PLACED write instead. - **`release-rbf.sh`'S STALENESS CHECK READS MTIMES, AND A BRANCH SWITCH REWRITES THEM (v2.8.2).** After the sibling's v2.8.2 PR merged, `git switch main && git pull` touched `rtl/apu2a03.sv`, `rtl/cart/cart.sv` and `rtl/ppu2c02.sv`, and `scripts/release-rbf.sh v2.8.2 --no-build` then refused: "RustyNES.sta.rpt is OLDER than 3 source file(s)". The content was identical -- `git diff b09ee32 927ef4a -- rtl/` (the compiled commit against the merge) was 0 lines -- so the report matched the tree and only the timestamps moved. The assets were uploaded with the script's own fallback command, `gh release upload v2.8.2 releases/.rbf --clobber`. The check is right to be conservative; the lesson is ORDER: stage and upload from the PR branch before switching, or prove content identity against the WORKING TREE before overriding it, never by argument: `git diff -- rtl/` (commit against working tree, so staged and unstaged edits count) must be empty AND `git status --porcelain -- rtl/` must print nothing (an untracked RTL file is an input too). `git diff HEAD` compares two commits and would miss an uncommitted edit (CodeRabbit on #559). diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md index 21f0c67a..35bfccf4 100644 --- a/to-dos/ROADMAP.md +++ b/to-dos/ROADMAP.md @@ -1477,7 +1477,7 @@ re-minted: `T-PS-dual-runahead` (ADR 0032; **DONE v3.1.0**), `T-MISTER-SAVESTATE | `T-NETPLAY-MESH-NATIVE` | Native 3-4 player netplay over `mesh_net.rs`, then mobile. **Desktop DONE v3.4.0** (direct IP and LAN, over the authenticated mesh with the Four Score); mobile (MOB-09) follows | NET-01, MOB-09 | v3.4.0 | | `T-MOVIE-INPUT-STREAM` | The Zapper, microphone, Vs. coins and FDS events in `.rnm`. **DONE v3.5.0** (`.rnm` format 7, `rustynes_core::ExtraInput`; every expansion device and Datach scans too; netplay unchanged) | CR-03 | v3.5.0 | | `T-MMC5-LATCH-VBLANK` | The MMC5's split-screen and ExGrafix CHR bank latches survive VBlank (found in the v3.5.0 sibling review). In the oracle, `split_chr_bank_latch` and `ex_chr_bank_latch` are cleared only by the next nametable fetch or a `$5104` write, and the MiSTer core matches it, so a `$2007` CHR read during VBlank uses the last rendered tile's bank. The NESdev MMC5 page suggests hardware may not; settle it from the page and a test ROM, not from either core. Gate: a stimulus that reads CHR through `$2007` in VBlank after a split or ExGrafix scanline, with the expected bank stated from documentation, passing in both cores. The same change corrects `mmc5.sv`'s scanline-detection comment ("the read after three matching nametable reads"), left alone at v3.5.0 so the tagged tree matched the compiled one | (#69 review) | v3.5.1 | -| `T-WIN-SCRIPT-IPC-CI` | The OS shared-memory store's Windows code ran in no CI job (found v3.5.0): the `test` matrix reaches Windows only on a full run and without `script-ipc`. **Wired 2026-10-23** (the v3.5.0 post-release PR): `full-build`'s Windows leg runs the `shm::` and `script_host::` tests with `script-ipc` and refuses an empty filter. Closes when that step has passed once, including v3.5.0's grow-while-held recovery test | (#624 review) | v3.5.1 | +| `T-WIN-SCRIPT-IPC-CI` | The OS shared-memory store's Windows and Apple code was compiled by `full-build` and tested by no CI job (found v3.5.0): the `test` matrix reaches Windows and macOS only on a full run and without `script-ipc`. **Wired 2026-10-23** (the v3.5.0 post-release PR): both `full-build` legs (Windows and macOS) run the `shm::`, `script_host::` and `memif::` tests on the `full` build, each group must show a passing test, and Windows must pass the grow test by name. Closes when that step has passed once on both | (#624 review) | v3.5.1 | | `T-V350-REVIEW-RESIDUE` | Antigravity suggestions on #624 and #69 not taken at v3.5.0, each to be confirmed or refuted against the code: a device key's latched state across a rebind; `lua_eval` from the MCP server while a real-time run is going; reads of user-chosen ROM, patch and splits paths with no size bound; and in the sibling, the dead `x_hit_q` register and the `nt_ovr` / IRQ outputs not gated by the board select. Gate: each item closed with a failing test and its fix, or with the reason it does not apply | (#624, #69 review) | v3.5.1 | | `T-LIBRETRO-OPTIONS-V2` | Core Options v2 and float-audio negotiation, behind capability checks | ecosystem 3 | v3.6.0 | | `T-LIBRETRO-CONSOLES` | Switch, PS Vita and 3DS investigated for the libretro core (D28) | LR-04 | v3.6.0 | From 31b0302008f93f7555ef272d22c0daccf04ff81b Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 23 Oct 2026 19:35:28 -0400 Subject: [PATCH 4/6] test(shm): plant the squatted object without fchmod on macOS The new full-build step's first run (CI 40039138613) did its job on both systems. Windows: 33 passed, among them a_larger_write_with_a_reader_attached_is_refused_and_the_region_kept, the v3.5.0 grow-recovery test, which had never run anywhere. macOS: 34 passed and one failed, shm::tests::a_squatted_object_with_open_permissions_is_refused, at shm.rs:902, `assert_eq!(libc::fchmod(fd, 0o666), 0)`. The cause is the fixture, not the check. macOS does not implement fchmod on a POSIX shared-memory descriptor, so the test died planting its squatted object, before it reached SharedRegion::open/create. The production check reads the mode with fstat, and every other shm test, create and open included, passes on macOS. On Apple the plant now keeps the mode shm_open gives it (0666 less the umask: 0644 under the usual 022, which others can read, so the owner rule must refuse it); elsewhere fchmod still forces 0666. Either way the test reads the mode back with fstat and expects exactly that "mode NNN" in the refusal, instead of assuming 666. It asserts the plant actually has group or other bits, so a runner with umask 077 fails loudly ("no squatted object can be planted") rather than passing a test that tested nothing. The u32::from on st_mode carries the same justified #[allow(clippy::useless_conversion)] as check_owner (u32 on Linux, u16 on macOS). Verified on Linux: cargo fmt --check; clippy -D warnings for the workspace and for rustynes-frontend --features full; the step's command, 35 passed, 1 ignored. Mutation: owner_ok reduced to `uid == me` (the mode half removed) makes the test FAIL at the open/create assertion; restored from a snapshot, diff confirmed. The macOS path is verified by this PR's next macOS run only; it cannot run here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj --- CHANGELOG.md | 7 ++++++- crates/rustynes-frontend/src/shm.rs | 31 ++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f70c7c7..b4304888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,12 @@ cycle-accurate core later replaced. holds. The `full-build` job compiled that code on both systems and no job ran its tests. Both legs now run the store's, the script host's and `memif`'s tests on the `full` build; each group must show a passing test, and on - Windows the grow test must pass by name (`T-WIN-SCRIPT-IPC-CI`). + Windows the grow test must pass by name (`T-WIN-SCRIPT-IPC-CI`). Its first + run passed on Windows, including the grow test, and failed one test on + macOS: the test that plants a shared-memory object other users can read + changed the object's mode with `fchmod`, which macOS does not support on + such objects. The refusal itself works there; the test now plants with the + mode the system gives a new object and reads that mode back. ### Documentation diff --git a/crates/rustynes-frontend/src/shm.rs b/crates/rustynes-frontend/src/shm.rs index a8356de1..68c53c7d 100644 --- a/crates/rustynes-frontend/src/shm.rs +++ b/crates/rustynes-frontend/src/shm.rs @@ -897,17 +897,38 @@ mod tests { }; assert!(fd >= 0, "plant: {}", io::Error::last_os_error()); // SAFETY: `fd` is the descriptor just opened. - unsafe { - assert_eq!(libc::ftruncate(fd, 4096), 0); - assert_eq!(libc::fchmod(fd, 0o666), 0); + assert_eq!(unsafe { libc::ftruncate(fd, 4096) }, 0); + // macOS does not implement `fchmod` on a POSIX shared-memory + // descriptor (it fails; found when CI first ran this test there, #625), + // so on Apple the plant keeps the mode `shm_open` gave it: 0666 less + // the umask, 0644 under the usual 022, which others can read and the + // check must refuse. The expected message is built from the mode + // actually read back rather than assumed. + #[cfg(not(target_vendor = "apple"))] + // SAFETY: `fd` is the descriptor just opened. + assert_eq!(unsafe { libc::fchmod(fd, 0o666) }, 0); + // `st_mode` is a u32 on Linux and a u16 on macOS, as in `check_owner`. + #[allow(clippy::useless_conversion)] + // SAFETY: `fd` is open; `st` is a valid out-pointer for `fstat`. + let mode = unsafe { + let mut st: libc::stat = std::mem::zeroed(); + assert_eq!(libc::fstat(fd, &raw mut st), 0); libc::close(fd); - } + u32::from(st.st_mode) & 0o777 + }; + assert_ne!( + mode & 0o077, + 0, + "the plant is mode {mode:03o}: this process's umask leaves the \ + object private, so no squatted object can be planted to test" + ); + let expected = format!("mode {mode:03o}"); for err in [ SharedRegion::open(&name).unwrap_err(), SharedRegion::create(&name, 4096).unwrap_err(), ] { assert_eq!(err.kind(), io::ErrorKind::PermissionDenied, "{err}"); - assert!(err.to_string().contains("mode 666"), "{err}"); + assert!(err.to_string().contains(&expected), "{err}"); } // Clean up the plant; then the normal path works again. // SAFETY: a valid C string naming the object planted above. From a5b0702cc1b41aa9bee32ed7448a3f8e0c77494b Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 23 Oct 2026 19:47:49 -0400 Subject: [PATCH 5/6] test(shm): unlink the planted object on every exit from the squat test Antigravity on 31b03020, both points taken. The test unlinked its planted object only at the end, so any failing assertion between shm_open and that line (ftruncate, fchmod, fstat, the new "plant has group or other bits" check, or the refusal checks themselves) returned early and left a named object in the system's POSIX shared-memory namespace, where it outlives the test process. A local Plant(CString) guard now owns the name from the moment shm_open succeeds and unlinks it on drop; the success path drops it explicitly at the point the old shm_unlink stood, before the fresh create. The struct sits at the top of the function because clippy's items_after_statements rejects it after the first statement. The comment above shm_open still said the fchmod "sets them regardless", which 31b03020 made false on Apple. It now says the fchmod forces 0666 outside Apple and that Apple keeps the umask-reduced mode. Evidence for 31b03020 itself, from CI run 40040552828 (the run this push supersedes): full build (macos-15) passed, the step's log showing `shm::tests::a_squatted_object_with_open_permissions_is_refused ... ok` and 35 passed, 0 failed; full build (windows-latest) passed. Verified on Linux: cargo fmt --check; clippy -D warnings for the workspace and for rustynes-frontend --features full; the step's command, 35 passed, 1 ignored. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj --- crates/rustynes-frontend/src/shm.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/rustynes-frontend/src/shm.rs b/crates/rustynes-frontend/src/shm.rs index 68c53c7d..175b3d26 100644 --- a/crates/rustynes-frontend/src/shm.rs +++ b/crates/rustynes-frontend/src/shm.rs @@ -884,10 +884,22 @@ mod tests { #[test] fn a_squatted_object_with_open_permissions_is_refused() { use std::ffi::CString; + // Unlinks the planted name when dropped, so a failing assertion below + // does not leave the object in the system's namespace (Antigravity, + // #625); dropped explicitly before the fresh create at the end. + struct Plant(CString); + impl Drop for Plant { + fn drop(&mut self) { + // SAFETY: a valid C string naming the object planted below; + // unlinking a name that is already gone only returns ENOENT. + unsafe { libc::shm_unlink(self.0.as_ptr()) }; + } + } let name = unique("sq"); let c = CString::new(os_object_name(&name)).unwrap(); - // SAFETY: a valid C string; a new object, mode 0666 (umask may clear - // some bits; the fchmod below sets them regardless). + // SAFETY: a valid C string; a new object, mode 0666 less the umask. + // Outside Apple the fchmod below then forces 0666; on Apple the plant + // keeps the reduced mode (see there). let fd = unsafe { libc::shm_open( c.as_ptr(), @@ -896,6 +908,7 @@ mod tests { ) }; assert!(fd >= 0, "plant: {}", io::Error::last_os_error()); + let plant = Plant(c); // SAFETY: `fd` is the descriptor just opened. assert_eq!(unsafe { libc::ftruncate(fd, 4096) }, 0); // macOS does not implement `fchmod` on a POSIX shared-memory @@ -931,8 +944,7 @@ mod tests { assert!(err.to_string().contains(&expected), "{err}"); } // Clean up the plant; then the normal path works again. - // SAFETY: a valid C string naming the object planted above. - unsafe { libc::shm_unlink(c.as_ptr()) }; + drop(plant); let r = SharedRegion::create(&name, 4096).expect("a fresh create works"); assert!(r.created()); assert!(SharedRegion::open(&name).is_ok(), "and so does an open"); From b50d71c6b35bacaf465807ba134912ad2bfa299b Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 23 Oct 2026 19:52:25 -0400 Subject: [PATCH 6/6] test(shm): require owner read/write on the planted object CodeRabbit on 31b03020. On Apple the planted object keeps the mode shm_open's 0666 is reduced to by the umask. The existing precondition only required a group or other bit, so an unusual umask such as 0700 gives mode 0066 and passes it; SharedRegion::open and ::create then open the object O_RDWR, the owner has neither bit, and both fail with the OS's EACCES before check_owner reads the mode. The test would report a raw permission error rather than the refusal it exists to check, or the precondition message saying why it could not run. A second precondition now requires mode & 0o600 == 0o600, with a message naming the umask as the cause. Outside Apple fchmod forces 0666, so neither precondition can bind there; the comment says so. Verified on Linux: cargo fmt --check; clippy -D warnings with rustynes-frontend --features full; the full-build step's command, 35 passed, 1 ignored. Supersedes CI run 40041659268, which was mid-run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj --- crates/rustynes-frontend/src/shm.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/rustynes-frontend/src/shm.rs b/crates/rustynes-frontend/src/shm.rs index 175b3d26..7a64bd50 100644 --- a/crates/rustynes-frontend/src/shm.rs +++ b/crates/rustynes-frontend/src/shm.rs @@ -929,6 +929,17 @@ mod tests { libc::close(fd); u32::from(st.st_mode) & 0o777 }; + // Both preconditions only bind on Apple, where the plant keeps the + // umask-reduced mode: an unusual umask such as 0700 leaves 0066, which + // passes the second check but which `open`/`create` cannot open read- + // write, so they would fail with an OS error before the owner check + // runs (CodeRabbit, #625). + assert_eq!( + mode & 0o600, + 0o600, + "the plant is mode {mode:03o}: this process's umask removes owner \ + read/write, so O_RDWR cannot reach the owner check" + ); assert_ne!( mode & 0o077, 0,