From 172d893cdfe4bfa3da1386f9e3acf5265a28c0a3 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Fri, 9 Oct 2026 20:23:30 -0700 Subject: [PATCH] test(albok): sshfs pathname-withdrawal test for the .browse/ view passes (unit 4.3 exposure gate) Replica of a browse view on nh3-albok, Linux sshfs 3.7.3 client in an ephemeral container on nh3-dev, N=5 per condition. Fresh open of a never-opened child through a cached listing: ENOENT 5/5 with sshfs defaults and 5/5 with hour-long client caches. Null control (no rename) opens 5/5; held fd and failed-rename positive controls read 5/5. Replica removed afterwards. Also closes the post-clear checks in persistent-memory: fv-ml1 GPU 0/1 plateau, nh3-nas usage, and the mislabelled sfsrv-ana pre-pbs4 item. --- persistent-memory.md | 6 +- .../oneoff/albok-browse-withdrawal/README.md | 65 ++ .../albok-browse-withdrawal/manifest.json | 1 + .../results-20261009-2019.json | 880 ++++++++++++++++++ .../albok-browse-withdrawal/run-trials.py | 183 ++++ scripts/oneoff/albok-browse-withdrawal/run.sh | 24 + .../albok-browse-withdrawal/setup-replica.py | 51 + stacks/albok-service/README.md | 9 +- 8 files changed, 1213 insertions(+), 6 deletions(-) create mode 100644 scripts/oneoff/albok-browse-withdrawal/README.md create mode 100644 scripts/oneoff/albok-browse-withdrawal/manifest.json create mode 100644 scripts/oneoff/albok-browse-withdrawal/results-20261009-2019.json create mode 100755 scripts/oneoff/albok-browse-withdrawal/run-trials.py create mode 100755 scripts/oneoff/albok-browse-withdrawal/run.sh create mode 100755 scripts/oneoff/albok-browse-withdrawal/setup-replica.py diff --git a/persistent-memory.md b/persistent-memory.md index b4a738b..b396aed 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -120,15 +120,12 @@ _As of 2026-10-09 ~1955 PT. The newest subsection is first; older subsections ca - **Nothing is mid-action.** No claims held (`scripts/ops-log claims`). eshpfi is pushed through this snapshot's commit. ⚠ The working tree AND index are shared with infra-hermes and subagents: commit with `git commit -- ` (auto-memory `feedback_shared_git_index_commit_pathspecs`). `graphify-out/GRAPH_REPORT.md` stays modified and uncommitted on purpose (auto-regenerated). - **Approved, NOT built (do not start before Prime says so after this snapshot):** SVOS app-traffic ingress for U13. Details, release (`afab123`, v0.0.9, schema 12) and gotchas are in the 2026-10-09 Recent-decisions entry; svos-dev's edit doc is `docs/operations/u13-svos-edit.md`; highseat-dev owns the schedule. - **Owed by me (no input needed):** - - albok `.browse/` exposure test before ANY share: pick a surface (sshfs `follow_symlinks` is the lead), rename `.browse/` on nh3-albok, and a fresh open through the client's cached listing must FAIL; also record the failed-rename case. Report on albok-dev's thread; the operator's user needs the restricted wings' groups on the share host. → `stacks/albok-service/README.md` - - fv-ml1 GPU 0/1 plateau read (Beszel `stats.g..mu` × 1.024) ~10-10; every seat restarted at 0034 10-08. - Destroy `zroot/ROOT/debian@pre-patch-20261008` on fv-ml1 ~10-15 if nothing regressed. - - sfsrv-ana `pre-pbs4` snapshot delete is unverified (infra-ops has no key there; ask infra-hermes). - - Re-read nh3-nas usage after the 10-08/09 GCs (79% at 10-07 2350). - nh3-svos housekeeping: releases `6c6abbe` and `a737a06` are no longer referenced by the front door and can be deleted (`ac42ace` holds the same static as the current `b588510`). - **Watching: Worldtree memory gate.** 10-09 FAILED on filing/user (9, 11, 8 of 12; median 0.75 < 0.80), the first fail that is not #416. Bootstrap from 27 prior runs puts P(median ≤ 9) ≈ 0.06 per batch, and the misses are the habitual fixtures, so it is not called a regression yet. But per-run means drifted 10.6 → 10.0 → 9.7 → 9.3 since the 10-08 fv-ml1 patch (writer + judge = flash-next on GPU 2). If the 10-10 batch also fails filing, infra-hermes frames it as a possible regression after that patch and sends it to me; then investigate flash-next. ⚠ worldtree-dev has not read mail in 6+ days, so the reports dead-end there (Prime's call, below). - **Watching: jekyll on `sec-reasoning` (cyberprev, GPU 0)** since ~1435 10-06 (Prime moved it off gen-large). sec holds 337k KV tokens. jekyll's ~180k context plus a long sec prompt (up to 192k) can overflow it, as gen-large did (preemptions; decode down to 2–16 tok/s). jekyll's context grows ~1k/call toward the 262k ceiling. It runs on the shared all-agents-local key; a key of its own would make its traffic attributable. - **Waiting on Prime:** + - albok `.browse/` share: the sshfs withdrawal test PASSED (Linux client). Before a share: which wings his nh3-albok user (`vh`) gets (`albok-read` = fleet + library, `albok-personal` = notes; `albok-feedback` stays default-deny), and, if the workstation is macOS, a re-run of `scripts/oneoff/albok-browse-withdrawal/` on it. - Who sees Worldtree gate failures while worldtree-dev is not reading mail (Prime, Miranda, or keep waiting). - Rotate the fv-ml1 BMC password? It was printed into a local session transcript on 10-08 (never left nh3-dev); infra-ops leans no. - albok's 200-object load check, 2.01× against a 2× bar (albok-dev: the ruling is the operator's). @@ -189,6 +186,7 @@ Every vLLM seat on a shared card (GPU 0, GPU 1) has byte-pinned KV (`--kv-cache- ## Recent decisions +- `[2026-10-09]` **Post-clear checks closed (2015–2025 PT):** albok `.browse/` sshfs pathname-withdrawal test PASSED (replica on nh3-albok, Linux sshfs 3.7.3 client in a container; fresh open ENOENT 5/5 default and 5/5 hour-long caches; null + positive controls ok; failed rename = path served; reported to albok-dev) → `scripts/oneoff/albok-browse-withdrawal/README.md`. fv-ml1 plateau (Beszel 120m, 42 h after the 0034 10-08 restart): GPU 0 flat at 5,606 MiB free (~3.9 GB at parakeet's cap), GPU 1 median 9,459 free (~3.3 GB at intern-decision's 32k peak), no creep; GPU 2 643 MiB free = flash-next util 0.96, single tenant, by design. nh3-nas `/volume1` 71% (79% on 10-07). The "sfsrv-ana pre-pbs4" item was a mislabel: the pre-pbs4 snapshots were pfi-pve VM 100 and nh3-pve VM 105, dropped by infra-hermes 10-05 and read gone 10-09. - `[2026-10-09]` **albok `library` building (fiction, nonfiction) live for unit 4.5; Nemi + albok-dev re-minted with library grants (token grants are immutable), old tokens revoked 1555 by albok-dev.** → `stacks/albok-service/README.md` - `[2026-10-09]` **Homepage: The High Seat's description no longer says "SVOS" (Prime: author it yourself); SVOS's now says "Internal network + mesh".** - `[2026-10-09]` **APPROVED by Prime (10-09), BUILD DEFERRED until after his snapshot/clear: SVOS app-traffic ingress for U13** (apps on nh3-dev reach the nh3-svos kernel). Shape agreed with svos-dev: (1) kernel `--app-listen 127.0.0.1:8410`, which never trusts loopback (svos-dev builds it in U9a); (2) a Caddy origin `svos-kernel.phasefinal.com` on 10.100.50.43, no Basic auth, bearer passed, app paths only, allowlisted to nh3-dev; (3) kernel→bridge over plain LAN HTTP; kernel→seat host (terminal) via nh3-dev's wildcard Caddy `svos-seathost.nh3.phasefinal.com` (TLS, because the leg carries secrets). nh3-dev is NOT on the mesh; the contracts now say "fleet network". `--app-listen` is ON MAIN (`68c9933`, 10-09). It ships in kernel **0.0.9** with schema **11 → 12** (migration 0012 rebuilds dispatches + summons, so the 'schema 11' acceptance line changes and a pre-refresh backup is mandatory). At the cutover the bridge gets three owner-approved runtime grants (app-postoffice → `app:app-miranda` write, `app:app-postoffice` write, `ns:pages` write, all autonomous) and installs as `svos-postoffice[push]` with `althing-core` from `~/development/althing` in apps-env. **Release is cut: svoperatingsystem `afab123` (tag v0.0.9)**: kernel 0.0.9, svos-postoffice 0.0.2, svos-terminal 0.0.3, svos-connector 0.0.7, svos-agent 0.0.3, svos-miranda 0.0.2 (U9a + U5b). The SVOS side of the cutover is written up in `docs/operations/u13-svos-edit.md` (app listener on nh3-svos, terminal app moved to nh3-dev with `[seat_host]`, bridge on nh3-dev with the converse mapping). Gotchas: the terminal app serves WITHOUT its seat host if the socket or launch dir fails (one stderr line); `serve` refuses `--app-listen` on the primary port. Build only when the edit is scheduled (highseat-dev owns the schedule) and NOT before the snapshot; the runbook refresh is ours at deploy. Tracking: svos-dev thread `01M4GPXSBKQHCQ30FVPH4R3N7G`. diff --git a/scripts/oneoff/albok-browse-withdrawal/README.md b/scripts/oneoff/albok-browse-withdrawal/README.md new file mode 100644 index 0000000..a39b056 --- /dev/null +++ b/scripts/oneoff/albok-browse-withdrawal/README.md @@ -0,0 +1,65 @@ +# albok browse view: sshfs pathname-withdrawal test (unit 4.3 exposure gate) + +The albok unit 4.3 contract gates any workstation exposure of a wing's `.browse/` on infra-ops +demonstrating, on the chosen share surface, that renaming `.browse/` away on the server denies a +**fresh open of a never-opened child through a listing the client has cached**. The rename is what +albok-service does when it withholds a view (`.browse/` → `.browse.withheld/`). + +**Result (2026-10-09 2019–2021 PT): PASS on sshfs `follow_symlinks`, Linux client.** Raw output: +`results-20261009-2019.json`; replica ids: `manifest.json`. + +## Harness + +- **Server:** a replica of a browse view on nh3-albok (`setup-replica.py`): 60 × 1 MiB objects at + `///`, `.browse/stash/` → `../..///`, the same shape + as the live view. Same root ext4 and the same OpenSSH `sftp-server` as `/srv/albok`. The live view + was NOT used: renaming it races albok-service's link/unlink and a failed mandatory unlink withholds + the wing. The replica was removed after the run (ops-log 2026-10-10T03:22:22Z). +- **Client:** ephemeral `debian:bookworm-slim` container on nh3-dev (`run.sh`): SSHFS 3.7.3, + libfuse 3.14.0, FUSE kernel interface 7.31, host kernel 6.1. Mount: `ro,follow_symlinks` plus SSH + options; the mount root is `.browse/` alone. +- **Per trial**, on a fresh mount: list `stash/` and stat every child (the client's caches now hold + the listing and every child's attributes); open+close child A; open child B and **hold** the fd; + rename on the server with `os.rename`; then immediately list again, stat C, open C (never opened + before), read B at offset 1 MiB − 8 KiB (past readahead, so it must reach the server), re-open A. +- **Conditions, N = 5 each**, distinct children every trial: + `null` (no rename), `default` (sshfs defaults), `aggressive` (`kernel_cache,entry_timeout=3600, + attr_timeout=3600,dir_cache=yes,dcache_timeout=3600`), `failed_rename` (target pre-created + non-empty, so `rename(2)` refuses with ENOTEMPTY). + +## Results + +| condition | rename | cached list after | stat C after | **open C (fresh)** | held B far read | re-open A | list at +25 s | +|---|---|---|---|---|---|---|---| +| null (null control) | none | 60 | ok | **opens, bytes ok** 5/5 | ok 5/5 | opens 5/5 | n/a | +| default | ok | 60 (stale) | ok (cached) | **ENOENT 5/5** | ok 5/5 | ENOENT 5/5 | ENOENT 5/5 | +| aggressive caches | ok | 60 (stale) | ok (cached) | **ENOENT 5/5** | ok 5/5 | ENOENT 5/5 | n/a | +| failed_rename | refused ENOTEMPTY | 60 | ok | **opens, bytes ok** 5/5 | ok 5/5 | opens 5/5 | n/a | + +The fresh open was attempted 2–6 ms after the rename returned. Every trial started and ended with +the server in the clean state (`.browse/` present, no `.browse.withheld`). + +- **The gate holds.** The client still listed the child and stat'ed it from cache, and the open + failed anyway: each sshfs open is an SFTP OPEN by path, resolved on the server, and hour-long + client caches do not change that. +- **Controls.** With no rename every open succeeds (the method does not manufacture failures). The + held fd keeps reading after the rename, and a refused rename leaves every path served, so the + instrument does register access that survives a rename (positive controls). +- **What a rename does NOT withdraw:** a file already open on the client keeps reading through its + SFTP handle. That is the contract's narrowed guarantee (new access by pathname only). +- **Failed rename:** the path remains and is served. The service cannot deny opens it could not + withdraw; its `BROWSE_WITHHELD` marker only stops publication. + +## Limits + +- **Proven for the Linux kernel FUSE client only.** A macOS workstation runs a different FUSE + backend (macFUSE kext, macFUSE FSKit, or fuse-t, which is NFS-backed and may cache handles the way + the contract warns NFS does). Re-run this harness on the real workstation before trusting that + surface: rebuild the replica, then run `run-trials.py` directly with `ALBOK_TEST_KEY`, + `ALBOK_TEST_KNOWN_HOSTS`, `ALBOK_TEST_MNT` (and `ALBOK_TEST_SRV`) set. +- **The mount root is a presentation choice, not a boundary.** The SSH user's authority is whatever + its groups can read: a member of the wing's read group can mount the wing root and see raw object + directories, sidecars with their sensitivity records, and git history. The contract already says + this ("the wing group's raw read is authority over files"). +- The committed `run-trials.py` adds env overrides and a umount fallback after the run; the container + defaults it ran with are unchanged. diff --git a/scripts/oneoff/albok-browse-withdrawal/manifest.json b/scripts/oneoff/albok-browse-withdrawal/manifest.json new file mode 100644 index 0000000..5b1143f --- /dev/null +++ b/scripts/oneoff/albok-browse-withdrawal/manifest.json @@ -0,0 +1 @@ +{"root": "/home/infra-ops/albok-browse-test/wing", "size": 1048576, "objects": [{"i": 0, "id": "993690c06b714bb0b190b4a991a0eefb", "name": "doc00.md", "size": 1048576, "sha256": "d3188f2ee538e1e7470ae8c8d54100155665f9764ebb1eec00c6e3641e76447b"}, {"i": 1, "id": "541f1d4f25544817834e279e4e8a19ac", "name": "doc01.md", "size": 1048576, "sha256": "acf21d3443474595e7bdfd9f79fb6af8fbdcedba9046da9b7fb1ef9531dfb45c"}, {"i": 2, "id": "a8291b97f9f7416d8ececb5a23ad2c5c", "name": "doc02.md", "size": 1048576, "sha256": "84ca032534e7f155ac05446643784c2e8a3b47ab7be4805e83fe400bddb9e343"}, {"i": 3, "id": "19bcc9fa02d9430ab974855e5048d7e5", "name": "doc03.md", "size": 1048576, "sha256": "8354a3f0125f2cd867ffca16e5b0507c663a7f7506ed9c9fe3a6edd1dca697bd"}, {"i": 4, "id": "99d98df8b5144413a0715715bfa25d4c", "name": "doc04.md", "size": 1048576, "sha256": "32d1f274a8e05bd8295ada0bb6391c61506af1f04efccbb98e74e3c39265c943"}, {"i": 5, "id": "5ab68c24288446ffb8d7cc8fd391d0f8", "name": "doc05.md", "size": 1048576, "sha256": "11f3b576d0dcb7cf500456f270d90d544f080eb170f5a0a76c69779ba2ab46b3"}, {"i": 6, "id": "91db712196b44cfb9d0f4e1ba9ea4fde", "name": "doc06.md", "size": 1048576, "sha256": "d203caccf337ebdb8c8606ced6dee2c1b3f9266c8b180209275a24ef4cb2e0d3"}, {"i": 7, "id": "0a8576ec09244ac9858e2c6a726538c0", "name": "doc07.md", "size": 1048576, "sha256": "41ac7cda32659b03c714aa830d6218e2356452e4506723d1e99d7a4ca7321994"}, {"i": 8, "id": "c024a22c334d4d3ba083c23b874a096d", "name": "doc08.md", "size": 1048576, "sha256": "57064125b2e94adc0b9eba6858b01eb6e470d29a0600c35d11de1bbc53b0f98b"}, {"i": 9, "id": "e96104a737204b68b06c070da8755a97", "name": "doc09.md", "size": 1048576, "sha256": "abfe8778af6554b1fb639743c7b18cf148e839447a045e04e4737587f4b469ef"}, {"i": 10, "id": "4a246bb13980407baab80b84d4706dce", "name": "doc10.md", "size": 1048576, "sha256": "4beb13c50ba6354c51ba43e9927e9283c32da5dab8bdb908007083a8fb4e0090"}, {"i": 11, "id": "e25415a44dda44cab5741a6e4fc218d7", "name": "doc11.md", "size": 1048576, "sha256": "8346d683fa15d7511e5369b66243df2165e7af0dbfc7510497d4e729a8976f17"}, {"i": 12, "id": "21ac0e914a604eb6b12ae56d7eb59284", "name": "doc12.md", "size": 1048576, "sha256": "6b8ce88e69f51ee5ec9f76acdbd6fa76faa0992aceac965f0593e2e95a583f50"}, {"i": 13, "id": "9328d04511ec4ba9ad8588e373fd3a9f", "name": "doc13.md", "size": 1048576, "sha256": "dfe6ca90732089466e445125c1049748e56b7cd7bcab5c9b39b486022220956f"}, {"i": 14, "id": "6e3151db92854903a7acdb2034f3eaeb", "name": "doc14.md", "size": 1048576, "sha256": "bab7ec01d160823937daa94b84d98d38a290f6c91ec266a24db7b16642560e55"}, {"i": 15, "id": "53bb957f00fd4df58f9ff0b504883bfa", "name": "doc15.md", "size": 1048576, "sha256": "62b4ca55ebb838be2b8228dd66fced51b11e585b6d2d0fe06c06ac505cdeee67"}, {"i": 16, "id": "ae079616e60145ffa4e2a0c1583a37aa", "name": "doc16.md", "size": 1048576, "sha256": "6c2def8b28d31df2cb52aa194e03a2010fadcaa75f36c0545e4719cfbfd5aab2"}, {"i": 17, "id": "6c816f335b6649339272a0c5811c3137", "name": "doc17.md", "size": 1048576, "sha256": "3a4dc43e9fe9f87024fb475f17a9d4a528b67626cb1828ae642a47366cb96f81"}, {"i": 18, "id": "614f98cc02b24c478d7c1395f191ad91", "name": "doc18.md", "size": 1048576, "sha256": "39520cf10b68b72331b6549b89120462e816c35a806bc577575ac036ca7b4d73"}, {"i": 19, "id": "075ccec673ce474e90443dff9de26459", "name": "doc19.md", "size": 1048576, "sha256": "8338f98a4146cfb540d4e305b169d6a1cdd35fe4cc7da8bc28ed304277731575"}, {"i": 20, "id": "5ccc1d88b9a149638612a6863a7fa10b", "name": "doc20.md", "size": 1048576, "sha256": "f016baff3b45bbbfc54d83da7e434e198a5203788ed55e5878d6b6cded36dbcb"}, {"i": 21, "id": "07d3adc3ee0b456b8f4348ad771755e9", "name": "doc21.md", "size": 1048576, "sha256": "3e4947aae2bd0d2bf2b43efbeb41694605251a3ff732fbf0b0eb924f34d40e88"}, {"i": 22, "id": "02f88f5a490f4ed7ae6de9a3677d2560", "name": "doc22.md", "size": 1048576, "sha256": "0daa64fb7b4ae13900fc29cf25968f08687efce289169ca7d16dfb4be4fd5a32"}, {"i": 23, "id": "f11f2d73933043a899e199add61e3f38", "name": "doc23.md", "size": 1048576, "sha256": "76c000a4bf82614adc2eb563f5ead85ea72ad96fcd47f3523b825c2c48d4adeb"}, {"i": 24, "id": "18a59782dc934456a6b5ffc3ea628c93", "name": "doc24.md", "size": 1048576, "sha256": "b396f9e8c5ccad835afa469d8dfad24332dc2f646186815ff7ad704b8fd6caa3"}, {"i": 25, "id": "7d4da4d04d66478ab184f925c7ce191d", "name": "doc25.md", "size": 1048576, "sha256": "542b1947b4a0e06df3e3d3ed7a16e9595d991ebf80a8189ed7fe2bff96c913e3"}, {"i": 26, "id": "a4d50034bf01421e9080cb00cd2e30a6", "name": "doc26.md", "size": 1048576, "sha256": "45a79aaad793d23ac517fe8f12caeb17d5c57b8466ad81e81102712b67e69f48"}, {"i": 27, "id": "347b82b2fab34bfda04e1fe2fa1de6aa", "name": "doc27.md", "size": 1048576, "sha256": "018e3da99246266e8befdf08a3e3f5efc665528c62efa9b704413d50fcfd7052"}, {"i": 28, "id": "e044742c10cb4d84ba56e4a2e2e2ae9e", "name": "doc28.md", "size": 1048576, "sha256": "4f18e981e37332f753b09bb1c5b4f15631f743ded24c87384873e73093a98fff"}, {"i": 29, "id": "26df9b76a41c4559aa6be6232060917a", "name": "doc29.md", "size": 1048576, "sha256": "c88258d86f9a78cd53bec70147a8a6fcfd7d9679d3f5071dd6fc6ecb6c5f06ba"}, {"i": 30, "id": "805d279b0c444786890a84b07825b026", "name": "doc30.md", "size": 1048576, "sha256": "2dfd44ee2a277323d4333cbb1228e92f8ebcd971eeff817c9d1c63e494cdfa8c"}, {"i": 31, "id": "1dfb1bc50b4441bebb74487feb0a26bc", "name": "doc31.md", "size": 1048576, "sha256": "662c3c362858b3ed83c346e8e0efe5e8c89c93952350c8e8bce1d1bafffed214"}, {"i": 32, "id": "7e9aa1cab5d84958bdf8b9709780bf7b", "name": "doc32.md", "size": 1048576, "sha256": "34adc1c7cf112b6d0e94bc2e880e99e670774826c300c1bef2e898cbcf2cee4f"}, {"i": 33, "id": "a0a88c134ff84d2bb4d1db67cb1d5811", "name": "doc33.md", "size": 1048576, "sha256": "e755a3d3d4762565a39920a8ecd51791d4b99b532ae5f696fa40dcad008fa52a"}, {"i": 34, "id": "06dca2905cba49b7a6dab926171d9d3b", "name": "doc34.md", "size": 1048576, "sha256": "b05ef76ffa1eff53dfbf63d422ad430409cbdcf610d74e6358e6ca73ad475e0a"}, {"i": 35, "id": "f0d20ecf40f148b1bc78db608a9c2ffd", "name": "doc35.md", "size": 1048576, "sha256": "15eee36c09d6f0d6592022e29b99359437a133797d547816b2b786fd73614bd9"}, {"i": 36, "id": "b5795a0176db4254823d8b52597d1214", "name": "doc36.md", "size": 1048576, "sha256": "1e57b283d404199fd3cc84768971c5f5ffa9688b25656e12f29c5e47fc151d68"}, {"i": 37, "id": "36a50e7292704920b5f4abf5d532e083", "name": "doc37.md", "size": 1048576, "sha256": "f3551202a27590bb638ba02e4c512f5db58a679e3237c908f7fef4e09ff4a1fa"}, {"i": 38, "id": "11e530da5e2947c6b01b1aa4a40d474d", "name": "doc38.md", "size": 1048576, "sha256": "6af2b829041637c33b806a6c602db6911dcd574af56cc61e8a7c6b626964c012"}, {"i": 39, "id": "8c9b6c5b6140472aa4b4d0309ebd8b8f", "name": "doc39.md", "size": 1048576, "sha256": "6719bf47cb41011379c4fbba5f2ec060c71beb2314c1491c811680d71f62551b"}, {"i": 40, "id": "86a024cc2c994d39b2bacbd373970c3d", "name": "doc40.md", "size": 1048576, "sha256": "ded63662453c9ea7a41e974e12da5a4d8fa43aba61b5a59f6b9eb39a35b90012"}, {"i": 41, "id": "30d63f64925a4c7d8c7a86f47146b895", "name": "doc41.md", "size": 1048576, "sha256": "e84fe13a9cc9989ed364659554da721a640d8fc31024249c128ded1857f51db1"}, {"i": 42, "id": "769b00a7ef824b859410149ed21573e6", "name": "doc42.md", "size": 1048576, "sha256": "2b38cf58b30bd8f99323094f038111e9c6b275fdc1cdeee61855ec50150f351c"}, {"i": 43, "id": "b66b96de8892480ba8b53dceb4d36c01", "name": "doc43.md", "size": 1048576, "sha256": "cd42765f57247769630f894144552c1ed2b6c417a5ea6978598af6159810bd9e"}, {"i": 44, "id": "7736d9a45b9744b89475e2f9e8f51760", "name": "doc44.md", "size": 1048576, "sha256": "2af9107d97a99778f7c3bf0aa2ff0a7e2668b553a0f31895b976616533ef7241"}, {"i": 45, "id": "9b91943a4ee5449295aff5ac46c356e7", "name": "doc45.md", "size": 1048576, "sha256": "582cbead79bdc4abb7b9939d0bc149a29477cf7327112f94cf5bfd6549289442"}, {"i": 46, "id": "e88093570d6844b2b37b99d9f58aaa77", "name": "doc46.md", "size": 1048576, "sha256": "d7b82477f129bb79b7507ae79af1160c8cf9384d3ef5efb509d569d6a204cb2b"}, {"i": 47, "id": "11745086469e4c7da556e49ea5d45b5f", "name": "doc47.md", "size": 1048576, "sha256": "c916e487cc72216732655dcb28b0490e784b25f821a1984c55f234c57231c25d"}, {"i": 48, "id": "e07bf3f976cc4664a62a04ac16bc1107", "name": "doc48.md", "size": 1048576, "sha256": "bfb69bb33744e4ad5d745193c04d1a16afac117c61eb784c19e821a68e506792"}, {"i": 49, "id": "c6d14a1d9ce44a6894089ef4c4633ec9", "name": "doc49.md", "size": 1048576, "sha256": "77b380f00e4783c10a2547556aa5c3fca3e7f3125f4a91d19eca59e4c9463558"}, {"i": 50, "id": "9661de8063aa44ea93628f7eb1a8ccac", "name": "doc50.md", "size": 1048576, "sha256": "058fbd992b8b10c3c9a21098e5be8533df6caf0f0e340407dc4050e410706a7c"}, {"i": 51, "id": "ff3220a18fdc4592b4450392fa0cdbd0", "name": "doc51.md", "size": 1048576, "sha256": "44763cc4d6506827b36dc46ce8a6d48f138b9c4f0fb35074956a1996ee09ca85"}, {"i": 52, "id": "2728583189094e489af72f25bb36d590", "name": "doc52.md", "size": 1048576, "sha256": "7e689e8230bff6c0ac5978706ed3c633aca8dd26b07cc22fda9adfdfdb520f5c"}, {"i": 53, "id": "b3419a4b362f4006a5fe0d4d5584b9f7", "name": "doc53.md", "size": 1048576, "sha256": "93639de841ddb6d27987534449304530258f41453b526b30e441d982aa4f1f27"}, {"i": 54, "id": "481bef3582044a3cbd8d9194d9d05a1d", "name": "doc54.md", "size": 1048576, "sha256": "d997635f5670038eb698af014d5a57b37539136335cc157b0d444e30bbc10b9a"}, {"i": 55, "id": "dd8fa15d53e14e238a8762c3437ad405", "name": "doc55.md", "size": 1048576, "sha256": "1490da7cfdb27f8aa859d5daa059ce637e3fd1bd8a58674c852775b5fbd13900"}, {"i": 56, "id": "026add5433a6429c8f9d9a46ee677e71", "name": "doc56.md", "size": 1048576, "sha256": "9e9318b0779f114b4e81ff44db4d9fe64c9196555dc549a0f09de2166cd8fb1c"}, {"i": 57, "id": "263da881a35c4aa3958326012cd1902e", "name": "doc57.md", "size": 1048576, "sha256": "cda3a781412042103ed9a1ae888ba6364247da6effe4f3f06ae8b00ee5839d7b"}, {"i": 58, "id": "723bc2a3a37b47f68e249ba9c8903c33", "name": "doc58.md", "size": 1048576, "sha256": "09d3fb921151a293ca7914501ead455ffbd5ab562a9b82037c987a1c56af51fa"}, {"i": 59, "id": "077e974ce88441b4bffe9df795738cce", "name": "doc59.md", "size": 1048576, "sha256": "acf39af28dfc662992e377b907620e7b1acf4ec5114a1554b8b51794aa56da8e"}]} diff --git a/scripts/oneoff/albok-browse-withdrawal/results-20261009-2019.json b/scripts/oneoff/albok-browse-withdrawal/results-20261009-2019.json new file mode 100644 index 0000000..6a65ff5 --- /dev/null +++ b/scripts/oneoff/albok-browse-withdrawal/results-20261009-2019.json @@ -0,0 +1,880 @@ +{ + "started_note": "times are monotonic seconds; see run.sh for wall clock", + "sshfs_base": [ + "ro", + "follow_symlinks", + "IdentityFile=/keys/id", + "UserKnownHostsFile=/keys/known_hosts", + "StrictHostKeyChecking=yes", + "BatchMode=yes" + ], + "aggressive": [ + "kernel_cache", + "entry_timeout=3600", + "attr_timeout=3600", + "dir_cache=yes", + "dcache_timeout=3600" + ], + "trials": [ + { + "cond": "null", + "trial": 0, + "A": "doc00.md", + "B": "doc01.md", + "C": "doc02.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "null", + "trial": 1, + "A": "doc03.md", + "B": "doc04.md", + "C": "doc05.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "null", + "trial": 2, + "A": "doc06.md", + "B": "doc07.md", + "C": "doc08.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "null", + "trial": 3, + "A": "doc09.md", + "B": "doc10.md", + "C": "doc11.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "null", + "trial": 4, + "A": "doc12.md", + "B": "doc13.md", + "C": "doc14.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "default", + "trial": 0, + "A": "doc15.md", + "B": "doc16.md", + "C": "doc17.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.018 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "late_list": { + "errno": "ENOENT", + "secs_after_rename": 25.003 + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "default", + "trial": 1, + "A": "doc18.md", + "B": "doc19.md", + "C": "doc20.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.017 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.004 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "late_list": { + "errno": "ENOENT", + "secs_after_rename": 25.001 + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "default", + "trial": 2, + "A": "doc21.md", + "B": "doc22.md", + "C": "doc23.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.014 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "late_list": { + "errno": "ENOENT", + "secs_after_rename": 25.003 + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "default", + "trial": 3, + "A": "doc24.md", + "B": "doc25.md", + "C": "doc26.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.019 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "late_list": { + "errno": "ENOENT", + "secs_after_rename": 25.003 + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "default", + "trial": 4, + "A": "doc27.md", + "B": "doc28.md", + "C": "doc29.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.018 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.006 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "late_list": { + "errno": "ENOENT", + "secs_after_rename": 25.002 + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "aggressive", + "trial": 0, + "A": "doc30.md", + "B": "doc31.md", + "C": "doc32.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.019 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.003 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "aggressive", + "trial": 1, + "A": "doc33.md", + "B": "doc34.md", + "C": "doc35.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.016 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "aggressive", + "trial": 2, + "A": "doc36.md", + "B": "doc37.md", + "C": "doc38.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.017 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "aggressive", + "trial": 3, + "A": "doc39.md", + "B": "doc40.md", + "C": "doc41.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.015 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "aggressive", + "trial": 4, + "A": "doc42.md", + "B": "doc43.md", + "C": "doc44.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 0, + "out": "renamed", + "err": "", + "secs": 0.013 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": false, + "errno": "ENOENT", + "secs_after_rename": 0.003 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": false, + "errno": "ENOENT" + }, + "restore": "renamed", + "server_after": "True False" + }, + { + "cond": "failed_rename", + "trial": 0, + "A": "doc45.md", + "B": "doc46.md", + "C": "doc47.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 3, + "out": "refused ENOTEMPTY", + "err": "", + "secs": 0.018 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.003 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "failed_rename", + "trial": 1, + "A": "doc48.md", + "B": "doc49.md", + "C": "doc50.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 3, + "out": "refused ENOTEMPTY", + "err": "", + "secs": 0.016 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.003 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "failed_rename", + "trial": 2, + "A": "doc51.md", + "B": "doc52.md", + "C": "doc53.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 3, + "out": "refused ENOTEMPTY", + "err": "", + "secs": 0.018 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.003 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "failed_rename", + "trial": 3, + "A": "doc54.md", + "B": "doc55.md", + "C": "doc56.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 3, + "out": "refused ENOTEMPTY", + "err": "", + "secs": 0.017 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.002 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + }, + { + "cond": "failed_rename", + "trial": 4, + "A": "doc57.md", + "B": "doc58.md", + "C": "doc59.md", + "server_before": "True False", + "pre_list": 60, + "pre_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "pre_B_ok": true, + "rename": { + "rc": 3, + "out": "refused ENOTEMPTY", + "err": "", + "secs": 0.018 + }, + "post_list": { + "n": 60 + }, + "post_stat_C": { + "ok": true, + "size": 1048576 + }, + "post_open_C": { + "opened": true, + "read": 4096, + "content_ok": true, + "secs_after_rename": 0.003 + }, + "post_held_B": { + "read": 4096, + "content_ok": true + }, + "post_reopen_A": { + "opened": true, + "read": 4096, + "content_ok": true + }, + "server_after": "True False" + } + ] +} \ No newline at end of file diff --git a/scripts/oneoff/albok-browse-withdrawal/run-trials.py b/scripts/oneoff/albok-browse-withdrawal/run-trials.py new file mode 100755 index 0000000..b3a6162 --- /dev/null +++ b/scripts/oneoff/albok-browse-withdrawal/run-trials.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Pathname-withdrawal test for an albok browse view shared over sshfs. + +Runs on the CLIENT (an ephemeral container, see run.sh) against the replica built +by setup-replica.py. The gate (albok unit 4.3, exposure ruling): rename `.browse/` +away on the server, then a FRESH open of a child that was never opened, through a +listing the client has cached, must FAIL. + +Per trial (fresh mount each time), three distinct children: + A opened and closed before the rename -> re-opened after it + B opened before the rename, fd HELD -> read at a far offset after it + C listed and stat'ed, never opened -> opened after it (the gate) + +Conditions (N trials each): + null no rename C must OPEN (null control) + default rename ok, sshfs defaults C must FAIL (the gate) + aggressive rename ok, hour-long caches C must FAIL (gate, worst-case client caching) + failed_rename rename refused (ENOTEMPTY) C OPENS: the path remains and is served +B is the positive control: a retained handle must still read after a rename, which +proves the instrument can see access that survives a rename. + +Usage: run-trials.py (JSON to stdout) +""" +import errno +import json +import os +import shlex +import subprocess +import sys +import time + +# Overridable so the same harness runs on a workstation (e.g. macOS) without the container. +SRV = os.environ.get("ALBOK_TEST_SRV", "infra-ops@10.100.50.44") +KEY = os.environ.get("ALBOK_TEST_KEY", "/keys/id") +KH = os.environ.get("ALBOK_TEST_KNOWN_HOSTS", "/keys/known_hosts") +MNT = os.environ.get("ALBOK_TEST_MNT", "/mnt/browse") +SSH_OPTS = ["-i", KEY, "-o", "BatchMode=yes", "-o", f"UserKnownHostsFile={KH}", + "-o", "StrictHostKeyChecking=yes", "-o", "ControlMaster=auto", + "-o", "ControlPath=/tmp/cm-%r@%h", "-o", "ControlPersist=900"] +SSHFS_BASE = ["ro", "follow_symlinks", f"IdentityFile={KEY}", f"UserKnownHostsFile={KH}", + "StrictHostKeyChecking=yes", "BatchMode=yes"] +AGGRESSIVE = ["kernel_cache", "entry_timeout=3600", "attr_timeout=3600", + "dir_cache=yes", "dcache_timeout=3600"] +CONDITIONS = [("null", [], None), ("default", [], "ok"), + ("aggressive", AGGRESSIVE, "ok"), ("failed_rename", [], "fail")] +LATE_LIST_DELAY = 25 # past sshfs's default 20 s cache timeout + +manifest = json.load(open(sys.argv[1])) +N = int(sys.argv[2]) +ROOT = manifest["root"] +SIZE = manifest["size"] +OBJS = manifest["objects"] +VIEW, WITHHELD = f"{ROOT}/.browse", f"{ROOT}/.browse.withheld" + + +def ename(e: OSError) -> str: + return errno.errorcode.get(e.errno or 0, str(e.errno)) + + +def content(o) -> bytes: + line = f"replica object {o['i']} {o['id']}\n".encode() + return (line * (SIZE // len(line) + 1))[:SIZE] + + +def remote_py(code: str, *args: str): + cmd = "python3 -c " + shlex.quote(code) + "".join(" " + shlex.quote(a) for a in args) + t0 = time.monotonic() + r = subprocess.run(["ssh", *SSH_OPTS, SRV, cmd], capture_output=True, text=True, timeout=60) + return {"rc": r.returncode, "out": r.stdout.strip(), "err": r.stderr.strip()[-300:], + "secs": round(time.monotonic() - t0, 3)} + + +RENAME = ("import os,sys,errno\n" + "try:\n os.rename(sys.argv[1], sys.argv[2]); print('renamed')\n" + "except OSError as e:\n print('refused', errno.errorcode.get(e.errno)); sys.exit(3)") +STATE = "import os,sys; print(os.path.isdir(sys.argv[1]), os.path.lexists(sys.argv[2]))" + + +def open_read(path, expect, offset=0, n=4096): + try: + fd = os.open(path, os.O_RDONLY) + except OSError as e: + return {"opened": False, "errno": ename(e)} + try: + os.lseek(fd, offset, os.SEEK_SET) + b = os.read(fd, n) + return {"opened": True, "read": len(b), "content_ok": b == expect[offset:offset + n]} + except OSError as e: + return {"opened": True, "read_errno": ename(e)} + finally: + os.close(fd) + + +def listing(d): + try: + return {"n": len(os.listdir(d))} + except OSError as e: + return {"errno": ename(e)} + + +def stat(p): + try: + st = os.stat(p) + return {"ok": True, "size": st.st_size} + except OSError as e: + return {"ok": False, "errno": ename(e)} + + +def trial(cond, extra, rename, k, slot): + A, B, C = OBJS[3 * slot], OBJS[3 * slot + 1], OBJS[3 * slot + 2] + rec = {"cond": cond, "trial": k, "A": A["name"], "B": B["name"], "C": C["name"]} + rec["server_before"] = remote_py(STATE, VIEW, WITHHELD)["out"] + if rec["server_before"] != "True False": + rec["aborted"] = "server not in the clean state" + return rec + if rename == "fail": # a non-empty target makes rename(2) refuse + remote_py("import os,sys; os.makedirs(sys.argv[1])", f"{WITHHELD}/blocker") + os.makedirs(MNT, exist_ok=True) + m = subprocess.run(["sshfs", f"{SRV}:{VIEW}", MNT, "-o", ",".join(SSHFS_BASE + extra)], + capture_output=True, text=True, timeout=60) + if m.returncode != 0: + rec["aborted"] = f"mount failed: {m.stderr.strip()}" + return rec + renamed = False + fd_b = None + try: + st_dir = f"{MNT}/stash" + names = os.listdir(st_dir) + rec["pre_list"] = len(names) + for nm in names: # cache every child's attrs: the client "knows" C exists + os.stat(f"{st_dir}/{nm}") + rec["pre_A"] = open_read(f"{st_dir}/{A['name']}", content(A)) + fd_b = os.open(f"{st_dir}/{B['name']}", os.O_RDONLY) + rec["pre_B_ok"] = os.read(fd_b, 4096) == content(B)[:4096] + + if rename: + rec["rename"] = remote_py(RENAME, VIEW, WITHHELD) + renamed = rec["rename"]["out"] == "renamed" + t0 = time.monotonic() + + rec["post_list"] = listing(st_dir) + rec["post_stat_C"] = stat(f"{st_dir}/{C['name']}") + rec["post_open_C"] = open_read(f"{st_dir}/{C['name']}", content(C)) + rec["post_open_C"]["secs_after_rename"] = round(time.monotonic() - t0, 3) + far = SIZE - 8192 # well past readahead: this read must go to the server + try: + os.lseek(fd_b, far, os.SEEK_SET) + b = os.read(fd_b, 4096) + rec["post_held_B"] = {"read": len(b), "content_ok": b == content(B)[far:far + 4096]} + except OSError as e: + rec["post_held_B"] = {"read_errno": ename(e)} + rec["post_reopen_A"] = open_read(f"{st_dir}/{A['name']}", content(A)) + if cond == "default": + time.sleep(max(0.0, LATE_LIST_DELAY - (time.monotonic() - t0))) + rec["late_list"] = listing(st_dir) + rec["late_list"]["secs_after_rename"] = round(time.monotonic() - t0, 3) + finally: + if fd_b is not None: + os.close(fd_b) + for um in (["fusermount3", "-u", MNT], ["fusermount", "-u", MNT], ["umount", MNT]): + try: + if subprocess.run(um, capture_output=True).returncode == 0: + break + except FileNotFoundError: + continue + if renamed: + rec["restore"] = remote_py(RENAME, WITHHELD, VIEW)["out"] + if rename == "fail": + remote_py("import os,sys; os.rmdir(sys.argv[1]); os.rmdir(sys.argv[2])", + f"{WITHHELD}/blocker", WITHHELD) + rec["server_after"] = remote_py(STATE, VIEW, WITHHELD)["out"] + return rec + + +results = [] +slot = 0 +for cond, extra, rename in CONDITIONS: + for k in range(N): + results.append(trial(cond, extra, rename, k, slot)) + slot += 1 +json.dump({"started_note": "times are monotonic seconds; see run.sh for wall clock", + "sshfs_base": SSHFS_BASE, "aggressive": AGGRESSIVE, "trials": results}, + sys.stdout, indent=1) diff --git a/scripts/oneoff/albok-browse-withdrawal/run.sh b/scripts/oneoff/albok-browse-withdrawal/run.sh new file mode 100755 index 0000000..6a85c59 --- /dev/null +++ b/scripts/oneoff/albok-browse-withdrawal/run.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Pathname-withdrawal test for the albok browse view over sshfs (unit 4.3 exposure gate). +# +# 1. ssh infra-ops@10.100.50.44 python3 - < setup-replica.py > manifest.json +# 2. ./run.sh [trials-per-condition] # default 5; JSON lands in results-.json +# 3. remove the replica on nh3-albok by its literal path when done +# +# The client is an ephemeral Debian 12 container on nh3-dev (sshfs 3.7.3 / libfuse 3), so +# nothing is installed on the host. It needs /dev/fuse and CAP_SYS_ADMIN to mount. +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +n=${1:-5} +stamp=$(date +%Y%m%d-%H%M) +out="$here/results-$stamp.json" +echo "start $(date '+%F %H%M %Z')" >&2 +docker run --rm --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor=unconfined \ + -v "$HOME/.ssh/infra-ops_ed25519:/keys/id:ro" \ + -v "$HOME/.ssh/known_hosts:/keys/known_hosts:ro" \ + -v "$here:/work" \ + debian:bookworm-slim bash -c " + apt-get -qq update >/dev/null && apt-get -qq install -y sshfs python3 openssh-client >/dev/null 2>&1 + sshfs --version >&2 + python3 /work/run-trials.py /work/manifest.json $n" > "$out" +echo "end $(date '+%F %H%M %Z') -> $out" >&2 diff --git a/scripts/oneoff/albok-browse-withdrawal/setup-replica.py b/scripts/oneoff/albok-browse-withdrawal/setup-replica.py new file mode 100755 index 0000000..31b0e27 --- /dev/null +++ b/scripts/oneoff/albok-browse-withdrawal/setup-replica.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Build a replica of an albok browse view on the share host (nh3-albok). + +Run ON the server as infra-ops, no sudo: + ssh infra-ops@10.100.50.44 python3 - < setup-replica.py > manifest.json + +Why a replica and not the live view: the test renames `.browse/` away. On a live +wing that rename races albok-service's own link()/unlink() calls, and a failed +mandatory unlink withholds the wing (BROWSE_WITHHELD marker + a failed write +transaction). The replica has the same shape as the real view +(`/.browse/` -> relative symlink `../..///`), sits +on the same filesystem (root ext4), and is served by the same sftp-server, which +is what the withdrawal property depends on. + +Content is deterministic per object, so the client can verify bytes without +fetching a reference copy. +""" +import hashlib +import json +import os +import uuid + +ROOT = "/home/infra-ops/albok-browse-test/wing" +COUNT = 60 +SIZE = 1 << 20 # 1 MiB: larger than FUSE readahead, so a late read must reach the server + + +def content(i: int, oid: str) -> bytes: + line = f"replica object {i} {oid}\n".encode() + return (line * (SIZE // len(line) + 1))[:SIZE] + + +os.makedirs(ROOT, exist_ok=False) # refuse to run over an existing replica +link_dir = os.path.join(ROOT, ".browse", "stash") +os.makedirs(link_dir) +objs = [] +for i in range(COUNT): + oid = uuid.uuid4().hex + obj_dir = os.path.join(ROOT, oid[:2], oid) + os.makedirs(obj_dir) + name = f"doc{i:02d}.md" + blob = os.path.join(obj_dir, name) + data = content(i, oid) + with open(blob, "wb") as f: + f.write(data) + os.chmod(blob, 0o640) + os.symlink(os.path.relpath(blob, link_dir), os.path.join(link_dir, name)) + objs.append({"i": i, "id": oid, "name": name, "size": SIZE, + "sha256": hashlib.sha256(data).hexdigest()}) + +print(json.dumps({"root": ROOT, "size": SIZE, "objects": objs})) diff --git a/stacks/albok-service/README.md b/stacks/albok-service/README.md index 73b76d0..fcdffd7 100644 --- a/stacks/albok-service/README.md +++ b/stacks/albok-service/README.md @@ -22,8 +22,13 @@ The FastAPI process that owns Albok's buildings-and-wings store (`vh/albok`, `/health` gains `browse_withheld`; 0 is normal, and a non-zero value is a WARN. No config change. ⚠ **`.browse/` is NOT shared with anyone yet.** The contract gates exposure on a demonstration on the chosen share: rename `.browse/` away on the server, then try a fresh open of an unopened child from the client through a cached - listing. That open must fail. sshfs `follow_symlinks` works by path; NFS/SMB may keep directory handles. Report a - passing surface to albok-dev, who tells the operator. Rollback: `compose.yaml.pre-0.1.14` (0.1.13) is free, because no + listing. That open must fail. sshfs `follow_symlinks` works by path; NFS/SMB may keep directory handles. + **[2026-10-09] PASSED on sshfs `ro,follow_symlinks` with a Linux client** (SSHFS 3.7.3 / libfuse 3.14): fresh open + ENOENT 5/5 with sshfs defaults and 5/5 with hour-long client caches, while the cached listing and stat still showed + the child; null and positive controls behaved (`scripts/oneoff/albok-browse-withdrawal/`). Reported to albok-dev. + Still before any share: the workstation's own client must re-run that harness if it is macOS (different FUSE + backend), and no user is in `albok-read`/`albok-personal` yet, so the operator's user needs those groups (the + operator's call; `albok-feedback` stays default-deny). Rollback: `compose.yaml.pre-0.1.14` (0.1.13) is free, because no sidecar shape changed. Prior: 0.1.13 @ `sha256:bea95617…` (2026-10-07 1135 – 1804) (healthy 1136:17, between walks). infra-ops deployed it at albok-dev's ask (althing 8413), from a clean archive of vh/albok `70b55ad7`