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.
This commit is contained in:
@@ -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
|
||||
`<wing>/<id[:2]>/<id>/<blob>`, `.browse/stash/<name>` → `../../<id[:2]>/<id>/<blob>`, 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.
|
||||
Reference in New Issue
Block a user