Repository navigation
Pkg3: exact source snapshot executable elsewhere - #11960
Conversation
…, 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>
…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>
|
Both findings from review 69557 are fixed at head d19becc, and they were both right. 1 — 2 — Separately, and larger: the 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>
|
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 The premise was wrong, not just the guard. A relative
Verified at this head: executor tree with no |
…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>
|
Fixed at head 3585940, and the finding is right on the substance with one correction to its prediction.
The clippy prediction did not hold, and saying so matters more than agreeing. Separately, the destination TOCTOU the side chat raised is fixed in this same head. The free-form parent is deleted: Six wet scenarios, eight witness claims, 25/25 unit tests, clippy and fmt clean at this head. |
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>
|
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.
Three further fixes in the same head, from the side-chat review, since they change what the PR claims:
Seven wet scenarios, eight witness claims, fmt clean at this head. CI on the previous head |
…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>
|
review 69677 is right, and it found independently the same defect the side chat reproduced at the same head. Fixed at 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 The repair is not flipping the arm. Exhaustion and completion were sharing one answer, and flipping A second hole in the same walk, from the side chat. Appending Both have discriminating reds: restore the On Nine wet scenarios, eleven witness claims. Separately: |
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>
|
review 69821 — both findings real, fixed at The two operations are deleted.
The bare 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. 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>
|
review 69841 — correct, and it is this PR's own lesson applied one variable over. Fixed at
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 The repair is to observe the uid and construct the path. Two things beyond the letter of the finding:
Eight wet scenarios, thirteen witness claims, compile-clean, fmt clean at this head. |
…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>
|
review 69871 — both findings real, fixed at
The principal is observed once. The finding's framing is the part worth keeping: this is the law the module argues one type over. Restructuring the selection also removed a bare I audited every symbol this PR introduces rather than only the two named, since the cause is now demonstrably recurring: Eight wet scenarios, thirteen witness claims, compile-clean, fmt clean at this head. |
…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>
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_storageowns immutable content-addressed objects,gunbc.fabric_storage_file_storerealizes them over a directory, andproduct.fabric.workalready 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'sSourceManifestRef, and the git tree the dispatch actuator materializes. This is not an unresolved-authority split:product.fabric.workalready elects the source manifest, andgunbc.scm's manifest is a different subject (a VCS commit).Implementation boundary
gunbc.fabric_source_snapshot(new) — capture and materialize, and nothing else.SourceCompositionis 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.std.fabric_storagealready walks and verifies, so there is no decoder to drift from the encoder.gunbc.scm.write_spine_ingresshas none.FabricObjectMissing/FabricObjectCorruptfrom 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_contractvalidation_operation_commandover aGunbcClaimValidation, bound toPinnedPublicPlusLocalRoot— the same words the dispatch path hands a worker, resolved through a declaredCapabilityRealization. No new task format, no parallel framework, no seed/V1 fallback. (This gives that arm a second production consumer; it does not retirepinned_public_plus_local_root_production_consumer_frontier, whose dissolution names a belt tick, and that row is untouched.)Evidence, at exact head
391bc98a54a62f4484085524349e3f04e4b65b8bPositive control. Three modules are captured from a scratch directory, materialized into a directory that is not this checkout, and a real
gunbc run --claim-runchild 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.gitand noCargo.toml— asserted at materialization time, and a route that shelled out togit archivewould 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: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 modegunbc.scm.scm_wet_execution_receiptwarns 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_runprocess_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 carryingCargo.tomlbesidedag/, 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-rootspelling 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 therunverb before anything reads the root:anchor_source_rootand panicked at exit 101; it now refuses at exit 2 naming the root.WorkspaceRootBasis, printed by the verb as[workspace-root] declared|discovered <path>. A selection nobody can observe is indistinguishable from a silent one.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-rootis cwd-relative. It is not:anchor_source_rootanchors a relative root against the workspace root, so--source-root dagnames<workspace>/dagwherever the process stands. Fromsrc/with--source-root ../dag, the cwd-first rule acceptedsrcas the base and the claims passed — spelling every key againstsrc/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 byreview 69568.Measured, before and after: a directory holding three modules, no
.gitand noCargo.toml, exited 101 atprocess_workspace_root; it now resolves three source roots and exits 0. The refusal control:Local regression evidence:
cargo test -p v1-compiler --lib process_workspace_root23 passed / 0 failed;cargo clippy --all-targets -- -D warningsclean;cargo fmt --all --checkclean. This is v1 seed Rust, admitted undergunbc.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_actuatormaterializes source withgit worktree addtwice, and the two are different facts:verification_checkout_git_binding_frontieragainstdispatch_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 pointgit worktree addcannot 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 thepublic/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:RelativeSourcePathis sole-constructed behindadmit_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 carriergunbc.scm.object_storeCorpusManifestEntryandgunbc.scm.write_spine_ingressboth 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 byfalse, 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 latermkdir -pand 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 whileSnapshotMaterializedis 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.
ProviderWorkspaceis sole-constructed behindallocate_provider_workspace, which creates it withmktemp -dand 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 honoursTMPDIR— so the parent was still selectable, through the environment instead of through an argument, and under a shared writable non-stickyTMPDIRa 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 directoryTMPDIRhappened 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_rootrequires 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_EXCLrefuses 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
lstator 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
.dagfilesystem surface — an opened directory handle plusopenat/mkdiratwithO_NOFOLLOW, so a path is resolved once and every later operation is relative to the handle that resolution produced.extdeps.shellandextdeps.filesystem.filesystem_ioare path-addressed without exception today, so this is a missing host capability, not an unwritten check. It is not retired by a pre-flightlstat, by canonicalizing the destination, by re-checking it afterwards, or by one operation gainingO_NOFOLLOWwhile its siblings stay path-addressed — the capability is the handle every operation in the sequence shares. When it lands,ProviderWorkspacecarries 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_modea_pathname_reresolves_its_ancestry_between_calls, found at mitigatable with ceiling structurally impossible, carrying the reproduction and the trigger above. Nothing is owed todocs/design-rung-drops.md.Review
review 69568found 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 69557requested changes on the first head; both findings were real and are fixed.snapshot_materialized_manifest_wirewas 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.manifesttook three different subjects from three call sites under a name asserting a fourth; the field is nowobject, 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