Skip to content

Fabric store: publish entries at a mode derived from the store's readers, not the umask - #12342

Merged
gunbai-bot[bot] merged 11 commits into
mainfrom
session/calm-cat-580
Sep 27, 2026
Merged

gunbai-bot[bot] merged 11 commits into
mainfrom
session/calm-cat-580

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Dashboard node adhoc-f7be727b-2e8 (fixes a wrong assumption in #12294).

Defect

On srv1 (2026-09-26), every file the CI principal (ghrunner) wrote under /opt/gunbc/fabric-storage/{objects,heads} was 0600. The serve principal (briansrls) could not read them. The service journal shows no failed reads yet, because the serve side hasn't read a CI-written object.

Cause (walked back up the chain)

  • extdeps.filesystem.rust_realization rust_file_write_create_new_fn_def opens its staging file with no mode. The seed interpreter runs the same code. So the published mode is 0666 & ~umask.
  • The runner's listener umask is 0022, but the fleet-converge steps that write the store run umask 077 in their credential prelude, in the same shell as gunbc run. Fabric store: owner/group/mode derived from its two writers (CI claim principal can write) #12294 assumed the umask would give 0644. It gave 0600.

Fix (the parent's rulings B, then A)

  • Interface: new Filesystem.WriteCreateNewWithMode { path, content, mode }. One canonical realization now takes declared_mode: Option<u32> and sets the bits on the staged inode before the hard link publishes it. None produces today's behaviour exactly, so every existing caller is unchanged. The v1 emitter gets a new verb and a typed FileWriteMissingModeInput refusal. Stage0 mirrors are regenerated, and --required-regen reports first_generation_equal=true.
  • Derivation: gunbc.managed_directory managed_directory_entry_mode derives each class's read bit per (writer, reader) pair. A file is owned by whoever created it; its group is the directory's group under setgid.
    • For the fabric store this gives 0444. briansrls is not in ghrunner's group (checked on srv1), so briansrls reaches ghrunner-written files only through the other-read bit.
    • Other-read is allowed only because neither area grants other-execute. The derivation refuses if a directory ever does.
    • No write bit is derived, because entries are create-only. The ruling said 0644; this is 0444 by the same derivation.
  • Store: the placement derives the mode (both areas must agree, or it refuses) and gives it to both writers through fabric_storage_placed_file_root. Serve returns 503 and the client returns a typed FabricStorageBindingRefused if the derivation refuses. The CAS store gains DeclaredModeCreateOnly for heads. Fixtures that build a root with no mode keep the umask behaviour.
  • Repair of existing files: the srv1 area ensure adds find <area> -maxdepth 1 -type f -user ghrunner -exec /bin/chmod u+r,g+r,o+r {} +. It runs unprivileged as the applier, who owns the 0600 files. It only adds bits, running it twice changes nothing, and no sudoers grant is needed.

Evidence (executed locally, private target dir)

  • Seed tests (write_file_create_new_tests): 13/13 pass.
    • a_declared_mode_is_published_regardless_of_the_umask: a forked child with umask 077 and declared 0644 gets 0644; with umask 022 and declared 0640 it gets 0640. Without the new mode handling it would publish the umask's mode instead.
    • Control an_absent_mode_keeps_the_umask_derived_mode: umask 077 gives 0600, umask 022 gives 0644.
  • .dag claims, all passing:
    • Wet store claim a_declared_entry_mode_is_the_published_mode_of_objects_and_heads: stat reads 0444 on the object and on the head generation, and a store with no declared mode does not come out 0444.
    • Model claims in test.claim.managed_directory_witness: the 0444 derivation; the owner reads through other and the second writer through group; a single writer derives 0400; and the refusal when the directory grants o+x.
    • The existing closure claim still passes.
  • Live-deploy claims (the_fabric_storage_area_ensure_repairs_the_ci_principals_entries, the_placed_fabric_store_derives_entry_mode_0444): still running locally; I'll add the result here.

RED as the brief worded it

"An object written by writer A is readable by writer B" can't be expressed in a single-uid fixture. It is covered at two levels instead: the class derivation over two principals (model), and the published mode under umask 077 (seed test and wet claim).

Seed Rust record (review 71505)

Hand-written seed Rust added: the interpreter's write_create_new_with_mode arm, None passed at the existing write_create_new arm, and two umask tests in write_file_create_new_tests.

  • No new path. There is still one canonical create-new function (extdeps.filesystem.rust_realization), widened by an Option<u32>. The seed, v1_rt and the generated realization carry identical bytes, and both_projections_carry_the_one_authority_verbatim checks that. None behaves exactly as before, and an_absent_mode_keeps_the_umask_derived_mode checks that too.
  • Seed size: the interpreter grows by one match arm. It mirrors the existing write_create_new arm and exists because the interpreter's file-transport verb dispatch is hand-maintained seed code, the same as the four existing verbs. No scaffold path is added, and none is deleted.
  • Emitter side: modeled in src/v1/05_emit.dag / 05_emit_rust.dag. The generated v1_compiler_emit*.rs files are regenerated from it, and --required-regen reports first_generation_equal=true.
  • Why it's admitted (DESIGN §3): v1 maintenance that serves the self-host program. The fabric store is a v2 production dependency, and this fixes a live defect in the shared create-new realization without adding a second one.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 3 commits September 26, 2026 11:13
…ers, not the umask

Every object and head the CI principal wrote to /opt/gunbc/fabric-storage was
0600 (srv1 2026-09-26), so the serve principal could not read them. The cause
is not an explicit mode: gunbc_file_write_create_new opens its staging file with
no mode, and the fleet-converge steps that write the store run `umask 077` in
their credential prelude in the same shell as gunbc.

- extdeps.filesystem: Filesystem.WriteCreateNewWithMode; the one canonical
  create-new realization takes Option<u32> and sets the bits on the staged inode
  before the hard_link publishes it. None is today's behaviour exactly.
- gunbc.managed_directory: managed_directory_entry_mode derives the entries' mode
  per (writer, reader) pair from the same dependents as the directory mode, and
  REFUSES other-read when the directory grants other-execute.
- The fabric placement derives 0444 (briansrls reads ghrunner-written entries
  through other; neither area grants o+x), both writers bind it, and the CAS
  store gains DeclaredModeCreateOnly for heads.
- The srv1 area ensure repairs existing entries: unprivileged
  find -user ghrunner -exec chmod u+r,g+r,o+r, additive and idempotent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…quoted argv

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lace bare get in touched files

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

gunbai-bot Bot commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review 71507 in cb64dcf.

  • Inline && join (the blocking finding): removed. I took the review's preferred fix rather than a marker: install_d_managed_command is back to the bare install, and the entry repair is a separate deploy_raw step emitted by ensure_dependency_steps through managed_directory_entry_repair_commands. A refused admission produces no repair step, and its install step is already the poison marker. the_fabric_storage_area_ensure_repairs_the_ci_principals_entries now reads the repair step itself and passes locally, as does the existing writability claim.
  • Seed Rust record: added to the PR body under "Seed Rust record".
  • Also in cb64dcf: the floor's UnimportedBareProvider refusal on the two files I touched. filesystem_io.dag now imports filter from v2.std.algebra. durable_cas_file_store.dag uses first(skip(...)) instead of bare get. The suggested provider for get was a test module, and a production module importing that would be a layer inversion. The CAS and fabric wet claims pass locally.
    — sent from calm-cat-580

gunbc-ci-auto-heal and others added 5 commits September 26, 2026 13:11
… row as ImportsFixed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…f passing it as a step

A latent main type error the floor now compiles because this change reaches its closure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… + local wet schedule)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/v1/stage0/src/v1_compiler_emit_rust.rs
gunbc-ci-auto-heal and others added 2 commits September 26, 2026 20:59
…laim_executor --regen-round-cost)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/v1/stage0/src/v1_compiler_emit_rust.rs
@gunbai-bot

gunbai-bot Bot commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Re the recurring advisory on hand-written seed Rust (reviews 71505, 71514, 71556, 71570, 71629): the admission reason and a record are in the PR body under Seed Rust record.

  • No new path: it widens the one canonical create-new function; it doesn't add a second.
  • What's hand-written: one interpreter match arm, mirroring the existing write_create_new arm.
  • Why it's admitted (§3): the fabric store is the v2 fleet's production dependency, and this fixes a live defect in the seed's shared create-new realization.
  • No scaffold path is added or deleted, so there is no deferral row.
    — sent from calm-cat-580

…d by --regen-round-cost; the next round converges with 0 stages)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit 57bc92e Sep 27, 2026
5 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/calm-cat-580 branch September 27, 2026 04:25
@briansrls
briansrls restored the session/calm-cat-580 branch September 27, 2026 07:18
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.

0 participants