Prime, 2026-10-09: "vh gets albok-everything". playbooks/nh3-albok-vh-viewer.yaml joins vh to albok-read, albok-personal and albok-feedback on nh3-albok and authorizes his four Macs' public keys for vh; verified at a fresh login that every wing's .browse/ lists and reads. The withdrawal harness gains a macOS driver (run-mac.sh: every mountable FUSE backend, a read-only smoke of the real wings, results copied back), env overrides for a non-container client, and summarize.py. Sanity-checked as vh from the Linux container before staging.
76 lines
5.2 KiB
Markdown
76 lines
5.2 KiB
Markdown
# 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.
|
||
|
||
## macOS run (staged 2026-10-09 2245, not yet run)
|
||
|
||
`run-mac.sh` runs the same trials from the operator's Mac, as `vh`, against a vh-owned replica at
|
||
`~vh/albok-browse-test/wing` (`manifest-mac.json`). It tries every FUSE backend the Mac can mount with
|
||
(the default, plus `backend=smb` / `backend=fskit` where fuse-t or macFUSE offers them), smoke-tests every
|
||
real wing's view as vh (counts only), and copies its results to `~vh/albok-browse-test/results/`. The vh
|
||
path was sanity-checked first from the Linux container (N=1 per condition, all four as expected).
|
||
|
||
mkdir -p ~/albok-browse-test && cd ~/albok-browse-test && scp 'vh@10.100.50.44:albok-browse-test/harness/*' . && zsh run-mac.sh
|
||
|
||
## 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.
|