Skip to content

Pkg3: exact source snapshot executable elsewhere - #11960

Merged
briansrls merged 19 commits into
mainfrom
session/clever-ferret-280
Sep 23, 2026
Merged

briansrls merged 19 commits into
mainfrom
session/clever-ferret-280

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Subject

One capability, closed through a real executed request: an executor receives the intended immutable source and runs against exactly those bytes, without the caller's Git object store and without touching its index.

The corpus already held both halves and nothing joined them. std.fabric_storage owns immutable content-addressed objects, gunbc.fabric_storage_file_store realizes them over a directory, and product.fabric.work already rules that a Work consumes a source manifest and that a Git tree id is one binding-level realization of it rather than the fabric's source type. What no module did was put a source tree into that store or rebuild one out of it. This PR is that join and the wet evidence that it serves a request.

Authority election (reported to the manager before building): three candidates answer "what is an immutable source snapshot" here — gunbc.scm's corpus manifest, product.fabric.work's SourceManifestRef, and the git tree the dispatch actuator materializes. This is not an unresolved-authority split: product.fabric.work already elects the source manifest, and gunbc.scm's manifest is a different subject (a VCS commit).

Implementation boundary

  • gunbc.fabric_source_snapshot (new) — capture and materialize, and nothing else.

    • SourceComposition is a closed variant (PublicOnlyComposition / PublicAndPrivateComposition), not a list of roots: what an executor must agree about before it is handed bytes is which origins it is receiving, because that is what its authorization is over.
    • A snapshot is three object kinds under one identity rule — content, entry (links a name object and a content object, body is the directory), manifest (links the entries, body is the composition wire). Nothing here parses: the structure is carried by links std.fabric_storage already walks and verifies, so there is no decoder to drift from the encoder.
    • Inclusions are explicit, never a walk. A tracked-file enumeration is a git question in a filesystem costume — the file an author just created is invisible to it, and that is exactly the file a build request most often needs. There is no directory walk for the same reason gunbc.scm.write_spine_ingress has none.
    • Integrity is not re-checked here. Missing and mutated bytes are FabricObjectMissing / FabricObjectCorrupt from the store's own verification, carried through rather than re-decided.
  • tools.source_snapshot_handoff (new) — the wet driver. Four scenarios, each in its own workspace.

  • test.claim.fabric_source_snapshot_witness (new) — five supplied-input claims over the composition wire and the root boundary. Explicitly not the evidence for the capability; they ask the one question a wet run cannot discriminate cheaply (can two different compositions ever render alike).

The request is not spelled here. Its argv comes from gunbc.roadmap_execution_contract validation_operation_command over a GunbcClaimValidation, bound to PinnedPublicPlusLocalRoot — the same words the dispatch path hands a worker, resolved through a declared CapabilityRealization. No new task format, no parallel framework, no seed/V1 fallback. (This gives that arm a second production consumer; it does not retire pinned_public_plus_local_root_production_consumer_frontier, whose dissolution names a belt tick, and that row is untouched.)

Evidence, at exact head 391bc98a54a62f4484085524349e3f04e4b65b8b

CTRL_BUILD_MODE=local gunbc run --source-root dag --source-root src/v2 \
  --entry dag/gunbc/instruments/source_snapshot_handoff.dag --function source_snapshot_handoff
[source-snapshot] PASS executed-request manifest=c90206b63f4e843e files=3 exit=0
[source-snapshot] PASS missing-object refused: object missing: 7322e5425cea39f3
[source-snapshot] PASS corrupted-bytes refused: object corrupt: a011ec697187a8e7 held bytes hash to 5a908348439d5574
[source-snapshot] PASS composition-mismatch refused expected=...;public=public found=...;public=public;private=dag
[source-snapshot] PASS escaping-path refused public/../../outside.dag: the path holds a parent-directory segment and would leave the destination
[source-snapshot] PASS duplicate-path refused .../gunbc-snapshot-4BARn3/dag/twice.dag
[source-snapshot] PASS unsafe-allocation-root refused the unsafe parent: the candidate parent is neither sticky nor owner-only (mode 777), so another principal could rename the workspace inside it
EXIT=0

Positive control. Three modules are captured from a scratch directory, materialized into a directory that is not this checkout, and a real gunbc run --claim-run child process resolves all three across three source roots and exits 0. Every one of the three files is load-bearing — the claim in the private root imports a declaration from each public root — so there is no arrangement of two of them under which the request succeeds. The destination holds no .git and no Cargo.toml — asserted at materialization time, and a route that shelled out to git archive would pass the first assertion and fail this one. The result names the source consumed (manifest=c90206b63f4e843e), joined in one fold with the child's exit.

Failure/mutation control that goes red. With the composition admission replaced by if false, re-run:

[source-snapshot] FAIL composition-mismatch at materialize: refused for the wrong reason: entry-outside-composition
EXIT=1

The control discriminates, and the located detail is informative: the per-entry boundary still refuses, but later and for a different reason, which is the whole point of admitting the composition before any byte moves — a refusal that arrives after the private bytes are on the executor's disk has refused the report, not the transfer. The mutation controls edit the store, not the destination: editing a materialized file would only establish that a compiler notices changed source.

A located refusal also fired for real during authoring and is recorded in the module: the first run refused with build refused for gunbc: ctrl-build: dispatching remote build via BuildBuddy — PATH cargo is a shim that dispatches to a remote amd64 builder, the failure mode gunbc.scm.scm_wet_execution_receipt warns about, caught by the driver rather than mistaken for a flake.

The obstacle, root-caused rather than worked around

A first cut of this PR ended with prepare_executor_tree, which planted an empty git repository and a workspace manifest at the destination so the seed CLI would consent to run there. It was named, typed and declared — and it was still a workaround, because "an executor with source and no repository" is exactly the capability this PR exists to provide. It is deleted.

The earliest unjustified boundary is v1_compiler.cli_run process_workspace_root. What the workspace root establishes is a base such that every source root and module file has a stable repo-relative spelling, so module-graph facts and content indices key alike. What it did was ask git for a checkout carrying Cargo.toml beside dag/, and panic when none answered — a proxy that is correct inside a checkout and can say nothing at all outside one. The base was in the request the whole time: a relative --source-root spelling is by definition relative to the directory the run starts in.

So the two rules partition a population rather than retry one question, and bind_process_workspace_root(source_roots) is called at the run verb before anything reads the root:

  • Discovery is asked first and remains the incumbent authority. Where a checkout exists it is the base, exactly as before this PR — no invocation that succeeds today resolves any differently.
  • The request names the base only where discovery has no answer to widen from — the executor with source and no repository. Then strictly: every declared root must be a relative path of ordinary components resolving under cwd; a root that is absolute or escapes cwd disqualifies the rule rather than being reinterpreted.
  • The two failure states are separate causes. "There is no checkout to discover a base from" and "a declared root could not be established" send a caller to different places, so they are different refusals, and a third names a root that is absent under a discoverable checkout — that case previously reached anchor_source_root and panicked at exit 101; it now refuses at exit 2 naming the root.
  • Neither rule applying is a refusal — exit 2 with a located cause, instead of aborting inside a helper thirty frames down. A panic carries no exit contract and no actionable diagnostic, which is what DESIGN §5 forbids at a boundary whose job is to refuse precisely. So the failure arm refuses and never widens.
  • The choice is returned and reported: a typed WorkspaceRootBasis, printed by the verb as [workspace-root] declared|discovered <path>. A selection nobody can observe is indistinguishable from a silent one.
  • The same derivation binds the checkout root, discharging for this population the dissolution that scaffold already named ("release bins receive checkout-root at spawn"), and collapsing two proxies for one directory onto one declared fact.

The ordering is a correction, and the measurement that forced it is worth stating. The first cut asked the request first, on the premise that a relative --source-root is cwd-relative. It is not: anchor_source_root anchors a relative root against the workspace root, so --source-root dag names <workspace>/dag wherever the process stands. From src/ with --source-root ../dag, the cwd-first rule accepted src as the base and the claims passed — spelling every key against src/ where discovery would have spelled them against the repository. Correct answer, divergent keys, no diagnostic, which is worse than a refusal. Found by re-running the rule from a subdirectory and independently by review 69568.

Measured, before and after: a directory holding three modules, no .git and no Cargo.toml, exited 101 at process_workspace_root; it now resolves three source roots and exits 0. The refusal control:

$ gunbc run --source-root nope --entry x.dag --function f      # from /tmp, not a checkout
error: no workspace root: every --source-root would have to resolve under the current directory
  /tmp/claude-1000, and none of its ancestors is a git checkout carrying Cargo.toml beside dag/.
  cause: ... remedy: ...
EXIT=2

Local regression evidence: cargo test -p v1-compiler --lib process_workspace_root 23 passed / 0 failed; cargo clippy --all-targets -- -D warnings clean; cargo fmt --all --check clean. This is v1 seed Rust, admitted under gunbc.v1_maintenance_standing's PURPOSE test: it serves the v2 program by making a compiler runnable against source that did not arrive as a checkout.

Disposition of what it replaces

gunbc.roadmap_dispatch_actuator materializes source with git worktree add twice, and the two are different facts:

  • The mutable attempt tree is not replaced and is not a second route to this fact. Its subject is a working tree an agent will commit from — branch, index, history, a remote to push to. A snapshot is immutable by construction and cannot be the carrier for a tree whose purpose is to be edited. That is a boundary between subjects, not a gap.
  • The detached verification checkout is the same fact and is kept as a declared binding with a trigger, filed as verification_checkout_git_binding_frontier against dispatch_verification_checkout_path_for_instance. Git remains the binding today because every verification executor is a process on the host that holds the repository, where the caller's object store is the executor's and this route buys nothing. Its dissolution: a verification executor that does not share the caller's repository, at which point git worktree add cannot serve it at all and this route replaces it — explicitly not satisfied by this module existing or by routing a local executor through a snapshot it did not need.

Containment and exact membership

Two defects the side-chat review found, both real, both fixed by construction rather than by a check.

Path containment was a text prefix. A manifest entry whose directory read public/../.. passed the public/ test, and the materialization then created that directory and wrote outside the destination — with every object hashing to the ref that named it. Integrity of the contents says nothing about where they land, and conflating the two turns a content-addressed store into an arbitrary-write primitive. Paths are now admitted: RelativeSourcePath is sole-constructed behind admit_relative_source_path, which refuses an absolute path, an empty segment, and a . or .. segment in each of its four textual positions — the whole path, the first segment, an interior segment, the last. Over a /-separated relative path there is no fifth position, so the disjunction is exhaustive rather than heuristic, and the witness claims all four. This is also the carrier gunbc.scm.object_store CorpusManifestEntry and gunbc.scm.write_spine_ingress both name as their own widening trigger.

Control: a manifest with valid hashes and an escaping path refuses before any out-of-root write. With the .. clause replaced by false, the escape materializes and the control goes red at exit 1 — so the wall is load-bearing, not decoration.

Materialization did not establish an exact tree. A caller-supplied destination kept whatever was already in it, and the executor then walked a stale file as though it had come from the snapshot — "the executor received the intended source" false in the direction nobody looks: not a missing file, an extra one. Duplicate logical paths overwrote each other while both verified. The destination is now created by the call inside a provider-allocated workspace, so membership is exact by construction; every file lands through Filesystem.WriteCreateNew, so a second entry naming one path refuses; and a partial tree is unreachable by any later call — each makes its own — while the receipt that says complete exists on no refusal arm.

And the destination is not a caller-named path at all. A mktemp-created child inside a caller-named parent made the tree's contents exact and left its location addressed by name: every later mkdir -p and every file write walks that name again from the root. Reproduced at the filesystem level — rename the child aside, symlink the name at another tree, and the whole snapshot lands there while SnapshotMaterialized is still returned naming the substituted path. Content integrity and destination integrity are different facts, and a content-addressed store that addresses its output by name has only the first.

So the parent stops being a parameter. ProviderWorkspace is sole-constructed behind allocate_provider_workspace, which creates it with mktemp -d and asserts mode 0700 rather than inheriting it, and the materializer accepts only that carrier. The attacker's precondition was write access to a parent the caller named, and there is no longer a parent a caller can name.

Deleting the parameter did not delete the choice. The first cut of that repair allocated with a bare mktemp -d, which honours TMPDIR — so the parent was still selectable, through the environment instead of through an argument, and under a shared writable non-sticky TMPDIR a different uid can rename the workspace. The side chat reproduced that with two non-root uids. This module claimed at that moment that no actor but the owner could substitute anything, and the claim was false: it depended silently on a property of whatever directory TMPDIR happened to name. A guarantee resting on an unexamined environment variable is not a guarantee.

So the candidate parent is observed and admitted before anything is created in it: admit_provider_root requires a directory that is either sticky — the rule that stops a principal renaming an entry it does not own — or owner-only, which no other principal can traverse. Anything else refuses and the allocation does not happen, and the creation uses an explicit template under the directory just admitted rather than re-reading the environment.

Control: a world-writable non-sticky parent is refused, and sticky and owner-only parents are admitted — the admitted halves are part of the control, because without them it would pass for an admission that refuses everything, which is a wall-shaped hole rather than a wall. Red: admit anything and it fails at exit 1.

What that closes, at its real strength. Given an admitted parent, no actor other than the owning uid can substitute anything on this route: it cannot rename the workspace, and it cannot write inside it.

Declared residual

What stays open: ancestry substitution by the owning uid. An actor holding the uid this process runs as can rename a directory component of the destination between two of the route's operations and leave a symlink in its place, and the remaining writes follow it.

Nothing narrower is open. Not the file — O_CREAT|O_EXCL refuses a symlinked final component, measured alongside the reproduction, so the residual is a directory component and never the leaf. Not any other principal — the 0700 workspace excludes them. Not the snapshot's contents — the store verifies every object against the ref that named it. It is the destination's ancestry, for one principal.

Why no check is offered instead. A one-time lstat or canonicalize before the writes moves the window rather than removing it, and every primitive reachable here re-resolves ancestry at the moment of the call. Per DESIGN §4b — ask whether the check's red is authorable before writing the check — a guard that cannot fire for the state it names is worse than absent, because it will be cited as coverage.

Next-rung trigger, at capability grain: directory-anchored filesystem primitives, openat-style, exist in the .dag filesystem surface — an opened directory handle plus openat/mkdirat with O_NOFOLLOW, so a path is resolved once and every later operation is relative to the handle that resolution produced. extdeps.shell and extdeps.filesystem.filesystem_io are path-addressed without exception today, so this is a missing host capability, not an unwritten check. It is not retired by a pre-flight lstat, by canonicalizing the destination, by re-checking it afterwards, or by one operation gaining O_NOFOLLOW while its siblings stay path-addressed — the capability is the handle every operation in the sequence shares. When it lands, ProviderWorkspace carries the handle instead of the path and the residual closes for every principal.

Rostered as a failure mode, not a rung drop, and the distinction is deliberate: a §4b(3) drop records a rung that held and was lowered. This class never held higher here — the route is new and this is its first honest reading — so it is filed as gunbc.recurring_failure_mode a_pathname_reresolves_its_ancestry_between_calls, found at mitigatable with ceiling structurally impossible, carrying the reproduction and the trigger above. Nothing is owed to docs/design-rung-drops.md.

Review

review 69568 found that the declared rule's guard admitted a .. root that resolves outside cwd; that is fixed, and the section above records both the measurement and the ordering correction it forced — the repair goes past the suggested component check to remove the divergence class entirely.

review 69557 requested changes on the first head; both findings were real and are fixed. snapshot_materialized_manifest_wire was dangling (no call site in the closure, DESIGN §3c) and its refusal arm answered "" — a fabricated value at a boundary whose whole purpose is to say which bytes were consumed; it is deleted rather than given a consumer it did not have. SnapshotEntryUnavailable.manifest took three different subjects from three call sites under a name asserting a fourth; the field is now object, naming whichever object could not be fetched, with the receipt recorded on the type.

Scope honesty

This is one executor on one host reading a store on that host's disk. It establishes that the bytes travel by content identity and that the caller's repository is never read or written — which is what makes the route serviceable across a boundary — and it does not establish that a remote executor works, because no remote one ran. Binding this to gunbc.fabric_storage_client's served store is the next step, not this one. File mode, symlinks, submodules and non-text content are part of the source-manifest contract and are not carried; a request whose correctness depends on any of them must not be routed here, and the module says so with a dissolution trigger rather than degrading quietly.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 2 commits September 21, 2026 13:10
…, and its wet handoff driver

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…real request against exactly those bytes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 21, 2026 13:48
…elete the marker workaround

The first cut planted an empty git repository and a workspace manifest at the
destination so the seed CLI would consent to run there. That answered the symptom
at the link where it appeared: 'an executor with source and no repository' is the
capability itself, so manufacturing a repository to obtain it is a workaround.

The earliest unjustified boundary is v1_compiler.cli_run process_workspace_root.
What the root establishes is a base every repo-relative module key is spelled
against; what it did was ask git for a checkout carrying Cargo.toml beside dag/,
and panic when none answered. The base was in the request all along: a relative
--source-root spelling is by definition relative to the directory the run starts
in. It is now bound at the verb before anything reads it, the same derivation
binds the checkout root (discharging that scaffold's own named dissolution for
this population), and a run that can name no base refuses at exit 2 with a located
cause instead of aborting inside a helper.

Measured: a directory with three modules, no .git and no Cargo.toml resolves three
source roots and exits 0, where it exited 101 before.

Also addresses review 69557: deletes the dangling snapshot_materialized_manifest_wire
and its fabricating empty-string arm, and gives SnapshotEntryUnavailable one subject
per field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Both findings from review 69557 are fixed at head d19becc, and they were both right.

1 — snapshot_materialized_manifest_wire dangling with a fabricating arm. Deleted. The review is correct on both halves: git grep returned only the definition, and the SnapshotMaterializeRefused => "" arm was a fabricated value at the one boundary whose purpose is to say which bytes were consumed. Its comment claimed a consumer that does not exist — the executed-request scenario renders the ref directly — so the honest repair is deletion, not inventing a caller to justify it.

2 — SnapshotEntryUnavailable.manifest carrying three subjects. The field is now object, and the receipt is recorded on the type: an operator reading manifest <ref> would have gone looking for a manifest that was never the thing missing. It is the meaning fork the module header warns about, reproduced inside the refusal family meant to remove it.

Separately, and larger: the prepare_executor_tree marker function this PR previously used to satisfy the seed CLI is deleted, and the cause is fixed at v1_compiler.cli_run process_workspace_root — the workspace root is now bound from the request at the run verb, and a run that can name no base refuses at exit 2 instead of panicking. A destination with no .git and no Cargo.toml now runs the request and exits 0. The PR body carries the derivation and the before/after measurement.

All four wet scenarios and the five witness claims are green at this head.

…erwise - and report which answered

Two defects in the first cut of the derivation, one found by re-running it from a
subdirectory and one by review 69568, which are the same defect:

The premise was wrong. A relative --source-root is NOT cwd-relative in this CLI:
anchor_source_root anchors it against the WORKSPACE root, so --source-root dag
names <workspace>/dag wherever the process stands. Preferring a cwd-derived base
therefore re-keyed invocations that work today. Measured: from src/ with
--source-root ../dag the cwd-first rule accepted src as the base and the claims
PASSED, spelling every key against src/ where discovery would have spelled them
against the repository - correct answer, divergent keys, no diagnostic.

So discovery is asked FIRST and remains the incumbent authority: where a checkout
exists it is the base, exactly as before, and no invocation that succeeds today
resolves differently. The request names the base only where discovery has no
answer to widen from, and then strictly - every root a relative path of ordinary
components resolving under cwd, with a root that escapes cwd disqualifying the
rule rather than being reinterpreted. Neither rule applying is a refusal.

The choice is returned as a typed WorkspaceRootBasis and reported by the verb
([workspace-root] declared|discovered <path>), because a selection nobody can
observe is indistinguishable from a silent one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

review 69568 is right, and the defect is fixed at head f203259 — I hit the same input independently while checking the claim the reviewer quotes, which is the honest reason it was worth checking rather than asserting.

The measurement. From src/ with --source-root ../dag --source-root ../src/v2, the cwd-first rule accepted src as the base and the claims PASSED — spelling every module key against src/ where discovery would have spelled them against the repository. Correct answer, divergent keys, no diagnostic. That is worse than the refusal the review predicts, because nothing goes red.

The premise was wrong, not just the guard. A relative --source-root is not cwd-relative in this CLI: anchor_source_root anchors it against the workspace root, so --source-root dag names <workspace>/dag wherever the process stands. My doc comment asserted the opposite and built the ordering on it. So the repair is not only the component check the review suggests — it is the ordering:

  • Discovery is asked first and remains the incumbent authority. Where a checkout exists it is the base, exactly as before this PR. No invocation that succeeds today resolves differently, and the divergence class the review found cannot arise, because the cwd rule never competes with a discoverable checkout.
  • The request names the base only where discovery has no answer to widen from — the executor with source and no repository. Then strictly: every root a relative path of ordinary components (Component::Normal) resolving under cwd; a root that escapes cwd or is absolute disqualifies the rule rather than being reinterpreted. That is the higher-rung form the review asks for.
  • Neither rule applying is a refusal, exit 2 with a located cause — so the failure arm refuses and never widens.
  • The choice is returned as a typed WorkspaceRootBasis and reported by the verb: [workspace-root] declared|discovered <path>. A selection nobody can observe is indistinguishable from a silent one.

Verified at this head: executor tree with no .git and no Cargo.toml → declared, request exits 0; this repository → discovered, identical to incumbent behaviour; /tmp with a bogus root → exit 2. cargo test -p v1-compiler --lib process_workspace_root 23/23, clippy -D warnings clean, fmt clean, all four wet scenarios and five witness claims green.

gunbc-ci-auto-heal and others added 2 commits September 21, 2026 16:34
…refusals

(A) Path containment was a text prefix check. A manifest entry whose directory
read 'public/../..' passed the 'public/' test and the materialization then created
that directory and wrote OUTSIDE the destination, with every object hashing to the
ref that named it. Integrity of the contents says nothing about where they land.
Paths are now admitted by construction: RelativeSourcePath is sole-constructed
behind admit_relative_source_path, which refuses an absolute path, an empty
segment, and a '.' or '..' segment in any of its four textual positions -- an
exhaustive enumeration over '/'-separated relative paths, not a sample. This is
the carrier gunbc.scm.object_store and gunbc.scm.write_spine_ingress both name as
their own widening trigger. Control: a manifest with valid hashes and an escaping
path refuses before any out-of-root write; with the '..' clause removed the
escape MATERIALIZES and the control goes red at exit 1.

(B) Materialization did not establish an exact tree: a caller-supplied
destination kept whatever was already in it, and duplicate logical paths
overwrote each other while both verified. The destination is now created by the
call through mktemp -d inside a caller-named parent, so membership is exact by
construction, and every file lands through Filesystem.WriteCreateNew, so a second
entry naming one path refuses. A partial tree is unreachable by any later call --
each makes its own -- and the receipt, which says 'complete', exists on no refusal
arm. This is also what answers the symlink question: the route creates only
directories and regular files into a tree that began empty.

(C) 'No checkout to discover' and 'a declared root could not be established' were
one refusal. They are now three distinct typed causes, and a misspelled root under
a discoverable checkout refuses at exit 2 naming the root, where it previously
reached anchor_source_root and panicked at 101.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ile the class

The side chat reproduced a substitution I reproduced again before repairing: with
the parent taken as a String, mktemp created the child and every later Mkdir.Parents
and WriteCreateNew addressed it BY NAME. Rename the child aside, symlink the name at
another tree, and the whole snapshot lands there while SnapshotMaterialized is still
returned naming the substituted path. Content integrity and destination integrity are
different facts, and a content-addressed store that addresses its output by name has
only the first.

So the parent stops being a parameter. ProviderWorkspace is sole-constructed behind
allocate_provider_workspace, which creates it with mktemp -d and ASSERTS mode 0700
rather than inheriting it, and materialize_source_snapshot accepts only that carrier.
The attacker's precondition was write access to a parent the caller named, and there
is no longer a parent a caller can name.

What that closes and what it does not, at real strength: no actor other than the
owning uid can substitute anything on this route. An actor running AS the owner is NOT
closed, and no pathname-addressed design closes it -- every operation available here
re-resolves its ancestry at the moment of the call, and a one-time lstat or
canonicalize would move the window rather than remove it. That residual is declared,
not papered over, with its next-rung trigger named at capability grain: directory-
anchored operations (an opened handle plus openat/mkdirat with O_NOFOLLOW), which no
primitive in this repository provides.

Filed as gunbc.recurring_failure_mode a_pathname_reresolves_its_ancestry_between_calls,
with the reproduction, the bound (O_CREAT|O_EXCL already refuses a symlinked FINAL
component, so the residual is ancestry and never the leaf), and ceiling 4.

Also: delete declared_workspace_root, dead since bind_process_workspace_root began
reading cwd itself (review 69610), and correct the run_verb comment, which still said
declared-first after the code became discovery-first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Fixed at head 3585940, and the finding is right on the substance with one correction to its prediction.

declared_workspace_root was dead and is deleted. It went dead when bind_process_workspace_root began reading cwd itself -- it needs it for the refusal text either way -- and the two tests call declared_workspace_root_from. That is DESIGN §3c in its purest form: a declaration whose only consumer is a doc comment, which I also corrected since it cited a symbol that no longer exists.

The clippy prediction did not hold, and saying so matters more than agreeing. cargo clippy --all-targets -- -D warnings is green locally both before and after the deletion, and CI on the previous head 5ab6c5f2aca passed all four lanes including clippy (run 35626489481). cli_run is pub mod in the lib, which is why dead_code does not fire on a pub(crate) item inside it. So this was not a merge-blocking red -- it was a dangling declaration, which is reason enough to delete it, and I would rather record the difference than let a green lane be cited as having caught something it did not.

Separately, the destination TOCTOU the side chat raised is fixed in this same head. The free-form parent is deleted: ProviderWorkspace is sole-constructed behind allocate_provider_workspace, which creates the workspace with mktemp -d and asserts mode 0700, and materialize_source_snapshot accepts only that carrier. The attacker precondition was write access to a parent the caller named, and there is no longer a parent a caller can name. What that does not close is an actor running as the owning uid, and no pathname-addressed design closes it -- the residual is declared with its trigger at capability grain (directory-anchored openat/mkdirat with O_NOFOLLOW, which no primitive here provides) and filed as gunbc.recurring_failure_mode a_pathname_reresolves_its_ancestry_between_calls with the reproduction.

Six wet scenarios, eight witness claims, 25/25 unit tests, clippy and fmt clean at this head.

gunbc-ci-auto-heal and others added 2 commits September 21, 2026 17:22
What stays open is ancestry substitution by the owning uid, and the module now says
so in one sentence rather than leaving a reader to derive it: not the file, which
O_CREAT|O_EXCL refuses to redirect; not any other principal, which the 0700 workspace
excludes; not the contents, which the store verifies.

The trigger is restated at capability grain - directory-anchored filesystem
primitives, openat-style, in the .dag filesystem surface - with what does NOT retire
it named explicitly, since a trigger satisfied by a pre-flight lstat or by one
operation gaining O_NOFOLLOW would be retired while the capability stayed dead.

Also records why this is a recurring_failure_mode row and not a section 4b(3)
declared drop: a drop is a rung that held and was lowered, and this class never held
higher here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he release result

(1) Deleting the parameter did not delete the choice. allocate_provider_workspace
used a bare mktemp -d, which honours TMPDIR - so the parent was still selectable,
through the environment instead of through an argument, and under a shared writable
non-sticky TMPDIR a different uid can rename the workspace. The side chat reproduced
that with two non-root uids. This module CLAIMED at that moment that no actor but the
owner could substitute anything, and the claim was false: it depended silently on a
property of whatever directory TMPDIR happened to name.

So the candidate parent is observed and admitted before anything is created in it:
admit_provider_root requires a directory that is either sticky or owner-only, and the
allocation uses an explicit template under the directory it just admitted rather than
re-reading the environment. New control: a world-writable non-sticky parent is refused
while sticky and owner-only parents are admitted - and the admitted halves matter,
since without them the control would pass for an admission that refuses everything.
Red: admit anything and the control fails at exit 1.

(2) The escape fixture had been weakened to public/.., which lands INSIDE the snapshot
directory, while the check still looked at the provider workspace - so the fixture had
stopped being an escape and the second half of the control asserted about a location it
would never have written to. Green, and vacuous. public/../.. is restored, which is
where the check looks. Red: remove the wall and it materializes.

(3) run_scenario discarded release_provider_workspace's result, so a scenario that
leaked a workspace - with the executor's whole materialized tree in it - still reported
PASS. Both cleanups are consumed now and each names its own step.

Also review 69627: the seven-arm capture refusal family had one consumer, which
answered all seven with the constant 'the capture refused'. It now has a wire, like its
materialize sibling already did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

review 69627 is right, and it is the sharper version of the point because the authority it cites is this PR's own module. Fixed at head 391bc98.

gunbc.fabric_source_snapshot keeps seven capture refusals apart precisely so that "the file is not there", "the file is empty", "this path is not under a root you declared" and "the store would not take it" do not arrive at one place — and the only consumer in the corpus answered all seven with the constant "the capture refused". A located family whose sole reader flattens it is worse than no family: it costs the authoring and review of seven arms and delivers the diagnostic of one, which is §3c with the data row present and the fold missing. capture_refusal_wire now discriminates all seven by subject, exactly as snapshot_refusal_wire already did for the materialize family — that asymmetry is what makes this a slip rather than a disagreement about the rule.

Three further fixes in the same head, from the side-chat review, since they change what the PR claims:

  1. The allocation root is now admitted. Deleting the destination_parent parameter did not delete the choice: allocate_provider_workspace used a bare mktemp -d, which honours TMPDIR, so the parent was still selectable through the environment, and under a shared writable non-sticky TMPDIR a different uid can rename the workspace. The module claimed at that moment that no actor but the owner could substitute anything, and that claim was false — it depended silently on a property of whatever directory TMPDIR happened to name. admit_provider_root now requires a parent that is sticky or owner-only, and allocation uses an explicit template under the directory it just admitted rather than re-reading the environment. New control, with a discriminating red: a world-writable non-sticky parent refuses, while sticky and owner-only parents are admitted — the admitted halves matter, because without them the control would pass for an admission that refuses everything.

  2. The escape fixture was vacuous and is restored. It had been weakened to public/.., which lands inside the snapshot directory, while the check still looked at the provider workspace — so the fixture had stopped being an escape and the second half of the control asserted about a location it would never have written to. public/../.. is restored, which is where the check looks; removing the wall makes it materialize and the control goes red.

  3. A leaked provider workspace no longer reports PASS. release_provider_workspace's result was bound to _released and discarded, so a scenario that leaked a workspace — with the executor's whole materialized tree in it — still passed. Both cleanups are consumed and each names its own step.

Seven wet scenarios, eight witness claims, fmt clean at this head. CI on the previous head a55f3e46cf5 was green on all four lanes.

gunbc-ci-auto-heal and others added 4 commits September 21, 2026 19:04
…der-selected

Two holes the side chat reproduced with two unprivileged uids, both real.

(1) A directory's mode protects its CONTENTS, not its own entry. A 0700 candidate
under a 0777 non-sticky grandparent is renameable by anyone who can write the
grandparent, so the workspace inside it is substituted whole while its own
permissions never change. The subject is the ancestry, every level from the base to
the root, and nothing less can answer.

(2) Sticky without an owner is not a protection. Sticky says an entry may be renamed
by ITS OWNER; a sticky directory owned by an untrusted principal is one that
principal may still rearrange. /tmp is safe because it is root-owned AND sticky, and
the first rule kept the second half and dropped the first.

So every level must be owned by this process or root AND must not be writable by
others unless it is sticky, walked by repeated /.. under a bound that refuses a
deeper base rather than admitting an unexamined level. TMPDIR is no longer consulted
at all - admitting whatever it names was the environment-shaped form of the
free-form parameter this module already deleted once. The provider names its own
candidates, XDG_RUNTIME_DIR then /tmp, and none qualifying is a refusal.

Controls: an owner-only base under a world-writable non-sticky grandparent refuses,
while root-owned sticky /tmp is ADMITTED - the admitted half matters, or the control
would pass for an admission that refuses everything. The third case, a sticky
directory owned by an untrusted uid, cannot be built by a harness without privilege,
so it is claimed over supplied observations against the same level_is_safe the wet
control executes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…this base

The floor at head 391bc98 reported verdict=FloorRefused unexpected_failures=6,
all in runner_capacity_plan_witness and runner_capacity_realize_witness. Attributed
rather than assumed: crossing binary against sources shows my .dag files pass inside
a main worktree and main's own witnesses fail inside mine, so the variable is the
BASE, not the change. a4f9491 on main re-derives exactly those six.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row said every non-owner principal was excluded, full stop. Without the ancestry
walk that was false; with it the claim is conditional on the admission having been
made. It now states both defects the side chat reproduced, the rule that answers
them, and the scope at exactly its strength - and that this is never isolation
between untrusted sessions sharing a uid, since two sessions at one uid are one
principal to every mechanism involved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two walk holes, both reproduced by the side chat and the first of them found
independently by review 69677.

(1) ancestry_walk answered SAFE when its level budget ran out, so an unsafe ancestor
above the bound was never examined - and the annotation beside it claimed a deeper
base refuses, so the prose asserted the wall while the arm was the hole. Flipping the
exhausted arm alone would refuse every walk, so the repair is a TERMINATING
OBSERVATION rather than a counter: / is its own parent, so a level whose device and
inode equal those of its own .. IS the root. Root-reached admits, budget-exhausted
refuses, and the two stop sharing an answer.

(2) Appending /.. walks the RESOLVED target's ancestry, so a symlinked component of
the candidate is never inspected and its owner can repoint it after admission. The
admission now canonicalizes ONCE and returns the canonical spelling as the admitted
root, so the caller never names an alias again and there is nothing left to repoint.

Controls, both with discriminating reds: a chain deeper than the bound refuses (restore
the SAFE arm and it is admitted), and a base reached through an alias is admitted under
its RESOLVED name (return the candidate instead and the alias survives). The alias
control asserts the property that removes the exposure rather than a staged attack,
because a harness on one uid cannot create a component owned by another principal, and
it says so.

The scope sentences in the module and the row are corrected again. The pattern is worth
more than either correction: a scope sentence written once does not track the code
underneath it, so each repair has to be read against the claim and not only against the
defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

review 69677 is right, and it found independently the same defect the side chat reproduced at the same head. Fixed at 74b69fd93ab.

The quoted contradiction is the part worth dwelling on: the comment and the ledger row both asserted that a base deeper than the bound refuses, and the arm answered AncestrySafe. So the sentence a reader checks asserted the wall while the arm underneath was the hole — worse than having neither, because the prose is what gets cited.

The repair is not flipping the arm. Exhaustion and completion were sharing one answer, and flipping AncestrySafe to a refusal there would have made every walk refuse — the walk had no way to know it was finished, only that it had counted enough. It now has a terminating observation: / is its own parent, so a level whose device and inode equal those of its own .. is the root. Root-reached admits; budget-exhausted refuses; the bound is a loop guard and nothing else.

A second hole in the same walk, from the side chat. Appending /.. walks the resolved target's ancestry, so a symlinked component of the candidate is never inspected and its owner can repoint it after admission — demonstrated with a uid-33 alias under a uid-65534 provider. admit_provider_root now canonicalizes once and returns the canonical spelling as the admitted root, so the caller never names an alias again and there is nothing left to repoint.

Both have discriminating reds: restore the AncestrySafe exhaustion arm and the deep-ancestry control is admitted (FAIL, exit 1); return the candidate instead of the canonical path and the alias survives admission (FAIL). The alias control asserts the property that removes the exposure rather than a staged attack, because a harness on one uid cannot create a component owned by another principal — and it says so rather than implying coverage it does not have.

On XDG_RUNTIME_DIR, which the finding notes is still consulted: that is deliberate and now safe for the reason the finding itself identifies. It is a candidate, not an admitted base — it goes through the same canonicalize-and-walk as /tmp, and a candidate whose ancestry is unprotected, over-deep, or unresolvable refuses rather than being used. What was unsafe was admitting an environment-named directory without that walk, which is what TMPDIR used to get.

Nine wet scenarios, eleven witness claims. Separately: a4f949154c7 on main re-derived six capacity witnesses that were failing at my previous base; main is merged and they pass at this head.

gunbc-ci-auto-heal and others added 3 commits September 21, 2026 23:17
Operator ruling after two more reproduced defeats of the walk: a cross-uid window
between realpath and the walk (uid 33 against a uid 65534 provider), and dev/inode
equality with '..' falsified by a bind mount of D at D/repeat, which shares a device
and inode with its own parent well below /.

Each repair had been a better heuristic over an arbitrary path, and what was being
defended is a caller-supplied prefix every property of which must hold AT THE MOMENT
OF USE - which no check delivers, because each observation is separated from that
moment by a window. So the state is removed rather than checked again: the provider
owns ONE fixed base, /gunbc or /tmp/gunbc-provider-<user>, with the
ancestry ENUMERATED as literals. Neither discriminator applies - there is no
attacker-supplied prefix, and running out of an enumerated list is completion rather
than exhaustion. The walk, its bound, the root observation and the arbitrary-path
admission are deleted; admitting a caller-named base is now a declared frontier whose
trigger is the same directory-anchored capability.

A fixed name is a predictable name, so creation and adoption are separate: plain
mkdir fails if the name exists at all, including as a symlink, so its success proves
this process created the directory and only that branch sets a mode. An existing base
is verified as it stands and NEVER chmodded; not ours at owner-only mode refuses, and
a denial of service is the right outcome against executing where somebody else
controls the location. The base is held to a stricter rule than its ancestry: root is
trusted to own an ANCESTOR and not to own the base, because the base holds the
snapshot.

And the control caught the repair itself. && and || IN THIS SUBSTRATE DO NOT
SHORT-CIRCUIT: written as created && !chmod(base), the guard chmodded the squatted
directory to 0700 and admitted it as compliant, reintroducing the adoption it was
written to prevent. Measured with a probe - false && Filesystem.Write(...) writes the
file. An effect may sit under control flow, never under a boolean operand, and the
idiom is removed from the instrument too. It was caught only because the control
asserts the squatted base was left UNTOUCHED, not merely that the admission refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g it

I reported the non-short-circuit property as something I had found and as worth
propagating. The corpus already measures it: test.claim.eval_model_probe carries three
enrolled probes - and_short_circuit_direct_probe asserts the marker file is GONE after
false && shell.Remove.RecursiveForce(...), with the polarity taken from the
measurement rather than from a reading of the source - and
v2.workflow.local_repo_wet_terminal records why they exist.

So the defect was real and the discovery was not. The module and the failure-mode row
now cite those symbols instead of presenting this session's probe as the measurement,
and the row records the honest version: the property was enrolled, and the code was
written as if it were not.

The row also keeps the half that generalizes: the control caught this only because it
asserts the squatted base was left UNTOUCHED rather than that the admission refused,
and the broken guard refused for the right reason while having already chmodded on the
way. Where a refusal is supposed to leave the world alone, leaving the world alone is
the half that discriminates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d a bare if true

review 69821, both findings real.

shell.Stat.DeviceInodeOf and shell.Link.Symbolic have zero consumers: they were the
root-detection and alias-fixture primitives of the walk-based admission this PR
withdrew. DeviceInodeOf was worse than dangling - its annotation asserted the rule
'/ is its own parent, so / and /.. report one device and inode' as the way a walk
knows it has reached the root, while the failure-mode row in this same PR records
that rule as FALSIFIABLE by a bind mount. A declaration shipping prose that the same
diff retracts is DESIGN section 4d's first arm, and section 4c's rule that an
annotation is never evidence a machine claim holds.

verify_provider_base opened with a bare if true wrapping its whole body - mechanical
residue from splitting establish_provider_base, carrying no condition and no meaning.
Removed, with the body lifted a level.

Audited every symbol this PR introduced for a consumer rather than fixing only the
two that were named: Path.Canonical, Mkdir.New, WorldWritableDirectory, IsSticky,
ModeOf and Id.UserName each have one, and root_reached, RootReached,
provider_root_ancestry_bound, ancestry_step_path and provider_root_candidates_note
are already gone with the walk. One stale citation to the renamed admit_provider_root
corrected in the same pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

review 69821 — both findings real, fixed at 7ce3129536c.

The two operations are deleted. shell.Stat.DeviceInodeOf and shell.Link.Symbolic were the root-detection and alias-fixture primitives of the walk-based admission this PR withdrew two heads ago; when the walk went, their consumers went with it and I left the declarations behind.

DeviceInodeOf was worse than dangling, and the finding names exactly why: its annotation asserted "/ is its own parent, so / and /.. report one device and inode" as the way a walk knows it has reached the root, while the failure-mode row in this same PR records that rule as falsifiable by a bind mount of D at D/repeat. A declaration shipping prose the same diff retracts is §4d's first arm and §4c's rule that an annotation is never evidence a machine claim holds — and it would have been read as a sanctioned technique by the next person needing one.

The bare if true { is gone, body lifted a level. It was mechanical residue from splitting establish_provider_base into create-then-verify, carrying no condition and no meaning.

I audited every symbol this PR introduced rather than only the two named, since the same cause — a withdrawn design leaving declarations behind — could have left more. Path.Canonical, Mkdir.New, WorldWritableDirectory, IsSticky, ModeOf and Id.UserName each have a consumer; root_reached, RootReached, provider_root_ancestry_bound, ancestry_step_path and provider_root_candidates_note were already deleted with the walk. That pass also turned up a stale citation to the renamed admit_provider_root, corrected here.

Eight wet scenarios, thirteen witness claims, module compile-clean, fmt clean at this head.

…vironment

review 69841, and it is my own lesson applied one variable over.

provider_base_layouts spliced the environment's XDG_RUNTIME_DIR into the base and
then HARD-CODED the ancestry as if that value were a literal: /run/user, /run, /.
Nothing established the value WAS /run/user/<uid>. Under XDG_RUNTIME_DIR=/home/me/run
the base is /home/me/run/gunbc while the fold inspects three directories that are not
its ancestors and never inspects /home/me or /home - so a world-writable real
grandparent passes uninspected while safe irrelevant directories are reported as the
ancestry. That is not a weaker check, it is a fabricated one: the list claims to be
the base's ancestry and is not.

The row already carried the rule that catches this - when a repair removes a
parameter, ask what now supplies the value - and I had dropped TMPDIR for exactly
that reason before admitting XDG_RUNTIME_DIR with an asserted ancestry.

So the uid is OBSERVED and the path is CONSTRUCTED: /run/user/<uid> built from id -u,
every component of both layouts a literal this module wrote or a uid it read, each
ancestry exactly the chain above its own base. The environment is not consulted at
all. A runtime home that does not exist needs no probe - mkdir of the base fails, the
layout refuses, and the /tmp layout is tried next.

The control's admitted half now ASKS THE MODULE for its layouts instead of re-spelling
the production base, which could have drifted from provider_base_layouts while the
control kept passing. The row records the sharper recognition rule: an ancestry list
is only evidence if it is derivable from the base rather than asserted beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

review 69841 — correct, and it is this PR's own lesson applied one variable over. Fixed at 428cc3867ec.

provider_base_layouts spliced the environment's XDG_RUNTIME_DIR into the base and then hard-coded the ancestry as if that value were a literal. Nothing established the value was /run/user/<uid>. The finding's scenario is exact: under XDG_RUNTIME_DIR=/home/me/run the base is /home/me/run/gunbc while the fold inspects /run/user, /run and / — three directories that are not its ancestors — and never inspects /home/me or /home.

That is worse than a weak check and worth naming precisely: the list claims to be the base's ancestry and is not, so a world-writable real grandparent passes uninspected while three safe irrelevant directories are reported as protection. A fabricated check reads as coverage.

And the row already carried the rule that catches it — "when a repair removes a parameter, ask what now supplies the value, because a default read from the environment is the same choice with no name on it" — which I wrote when dropping TMPDIR, and then did not apply to XDG_RUNTIME_DIR two functions later.

The repair is to observe the uid and construct the path. /run/user/<uid> is built here from id -u, so every component of both layouts is a literal this module wrote or a uid it read, and each ancestry is exactly the chain above its own base. The environment is not consulted at all. A runtime home that does not exist needs no probe: mkdir of the base fails, that layout refuses, and the /tmp layout is tried next.

Two things beyond the letter of the finding:

  • The control's admitted half now asks the module for its layouts rather than re-spelling the production base. A control that keeps its own copy of the spelling passes while production drifts away from it — the same "asserted beside it rather than derived from it" defect, in the test.
  • The row records the sharper recognition rule the finding implies: an ancestry list is only evidence if it is derivable from the base, not asserted beside it. The earlier phrasing caught the environment-supplied base and missed the asserted ancestry.

Eight wet scenarios, thirteen witness claims, compile-clean, fmt clean at this head.

gunbc-ci-auto-heal and others added 3 commits September 22, 2026 01:55
…ion to redirect

The base was created with mkdir and then narrowed with chmod 0700, and the chmod
addressed it BY NAME - so a principal able to rename entries in the parent could swap
the fresh directory for a link between the two calls and send the chmod elsewhere,
leaving the base at whatever the umask gave it. Even with no swap, the name existed
briefly at a wider mode, long enough for another principal to open it and keep a
descriptor.

mkdir -m 0700 removes both: the directory is never present at any other mode and
there is no follow-up call to aim.

The reasoning-based answer would also have held here - for /tmp, sticky and root-owned
means only the entry's owner may rename it, and the fresh entry is ours; for
/run/user/<uid>, mode 0700 means no other principal can traverse it - and that is
exactly why it is not the version to ship. A window closed by argument has to be
re-argued every time the surrounding code moves; one that does not exist does not.

This also deletes the last guarded effect in the function, which is where the
non-short-circuiting && had put a chmod on a squatted directory.

The squat control's two halves now sit at different rungs and the annotation says so:
the refusal half still discriminates (neutralise base_is_admissible and the scenario
reports the squatted base as adopted, exit 1), while the untouched half cannot fail
against the current module and is a regression control over a landed wall - section
4b(4)'s flip rather than a retirement. It goes red again the moment a second pathname
operation reappears between creating the base and trusting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y from one chain

Three repairs, the first of which answers the question directly: NO, the ancestry was
NOT checked before the mkdir. establish_provider_base created the base and then
examined the tree above it, so a base under an unprotected ancestry was brought into
existence and only afterwards refused - leaving an owner-only directory inside a tree
this module had just decided it does not trust, for anybody watching that parent to act
on. A refusal after the mutation has refused the report and not the act, which is the
ordering argument this module already makes about admitting a composition before any
byte is written. The ancestry check now runs first and nothing is created on the
refusing path.

Second, base and ancestry are derived from ONE chain. They were two fields filled in
separately, which is the state that let an environment-supplied prefix carry an
ancestry belonging to a different path. provider_layout takes the chain root-first,
builds the base inside its deepest member and returns that same chain as the ancestry,
so handing it one and getting the other is not expressible. That is also what lets the
control ENTER THROUGH THE REAL CONSTRUCTION: it supplies a chain and gets its ancestry
derived by the same function production uses, instead of authoring a shape production
never produces.

Third, every ancestry level must be its own canonical spelling. stat FOLLOWS a symlink,
so a level that is one reports its target's owner and mode while the base is created
inside that target - the enumerated chain would describe a tree the base does not live
in. Checked rather than assumed of the literals, because /run/user/<uid> is provisioned
by the host and this module does not decide what it is.

The control now asserts the base DOES NOT EXIST after the refusal, and that half
discriminates: move the mkdir back above the ancestry check and it reports 'the refusal
was correct but the base had already been created under the unsafe parent'. Its fixture
is exactly what the module requires of its own base - canonical, owned by this process,
created at 0700 by the call under test - so the only defect is the parent and a refusal
can only be the ancestry rule firing.

Also corrects the wording: /run/user/<uid> is root-PROVISIONED and USER-owned. Calling
it root-owned made the level rule look like it relied on something it does not; what it
relies on is that this process's own uid is trusted for an ancestor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review 69871, both findings real, and the first is residue I created in the same
commit that deleted the previous round's residue.

shell.Mkdir.New had no consumer: replacing the mkdir+chmod pair with NewOwnerOnly left
it behind, exactly as withdrawing the walk had left DeviceInodeOf and Link.Symbolic.
Deleted, and its rationale folded into the operation that kept it - the failure on an
existing name is what makes creation proof of creation, which is the security boundary
rather than a convenience.

TrustedOwners.me and ProviderPrincipal.name were the same fact from two independent
id -un reads, called by one selection. Two reads of one fact can disagree, so the name
in /tmp/gunbc-provider-<name> and the name the level rule trusts could come from
different readings. The principal is now observed ONCE and trusted_owners_of derives
the predicate from it. That is the law this module argues one type over - the base and
its ancestry were two values a caller filled separately until they were derived from
one chain - and the instrument derives the same way rather than observing again.

Restructuring the selection also removed a bare if true I had just introduced while
making that change, which is the residue class the previous round flagged.

Audited every symbol this PR introduces, not only the two named: each of
NewOwnerOnly, Path.Canonical, WorldWritableDirectory, IsSticky, ModeOf, Id.UserName
and Id.UserId has exactly one consumer, and the one remaining mention of the deleted
observer is prose that now describes it instead of citing a name that no longer
resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

review 69871 — both findings real, fixed at f43cb0ea5d0.

shell.Mkdir.New is deleted, and it is residue I created in the same commit that deleted the previous round's residue. Replacing the mkdir+chmod pair with NewOwnerOnly orphaned it, exactly as withdrawing the walk had orphaned DeviceInodeOf and Link.Symbolic. Its rationale is folded into the operation that survived — failing on an existing name is what makes creation proof of creation, which is the security boundary rather than a convenience.

The principal is observed once. TrustedOwners.me and ProviderPrincipal.name were the same id -un fact from two independent reads, and select_provider_root called both — so the name in /tmp/gunbc-provider-<name> and the name the level rule trusts could come from different readings. trusted_owners_of now derives the predicate from the single observed principal, and the instrument derives the same way rather than observing again.

The finding's framing is the part worth keeping: this is the law the module argues one type over. provider_layout's header exists because the base and its ancestry were two values a caller filled separately until they were derived from one chain — and the identity of the principal was still two reads at the same time.

Restructuring the selection also removed a bare if true { I had just introduced while making that change — the residue class review 69821 flagged, reintroduced two heads later.

I audited every symbol this PR introduces rather than only the two named, since the cause is now demonstrably recurring: NewOwnerOnly, Path.Canonical, WorldWritableDirectory, IsSticky, ModeOf, Id.UserName and Id.UserId each have exactly one consumer. The one remaining mention of the deleted observer is prose, which now describes it rather than citing a name that no longer resolves.

Eight wet scenarios, thirteen witness claims, compile-clean, fmt clean at this head.

Merged via the queue into main with commit d83175e Sep 23, 2026
4 checks passed
@briansrls
briansrls deleted the session/clever-ferret-280 branch September 23, 2026 02:03
gunbai-bot Bot pushed a commit that referenced this pull request Sep 23, 2026
…pair as its own Spawned basis

Main's #11960 made the process workspace root a bound pair with a typed bind
refusal. The GUNBC_WORKSPACE_ROOT arm now answers first in both the panicking
resolver and bind_process_workspace_root, reports basis=spawned rather than
discovered, and refuses a non-locus through the bind's typed Err. The scaffold
census re-derives with -w to 87 added lines (12 -> 99).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant