Skip to content

id(world-compile): duplicate file names in a compile request are never refused — the later file silently overwrites the earlier, and source_blake3 provenance describes sources that were never compiled #1593

Closed

Description

@dezren39

Problem

A world compile request (ClientFrame::Compile → CompileSpec) may carry two files with the same name. Nothing refuses that, and every stage of pkgs/id/src/world_compile.rs then treats the request differently:

  1. validate_spec (world_compile.rs:146-185) checks the file count, that files[0] is main.roc, per-name safety, and per-file/total sizes — but never that names are unique.
  2. digest_sources (:423-442) hashes all files in the spec, so source_blake3 in the installed module's ModuleProvenance::SourceBuild (:213, :335-343) describes a source set that includes a file which is never compiled.
  3. The staging loop (:218-221) writes each file to job.path().join(name) in order, so a later same-name file silently overwrites the earlier one. The compiler only ever sees the last file of each name, and the main.roc read back for the platform-path rewrite (:224-228) is the last main.roc, not the files[0] the validator checked.
  4. check_imports (:118-144) scans all of spec.files, including shadowed files that never reach the compiler — a dead file importing an ungranted platform module refuses a compile whose actual sources are clean.

The session layer passes the wire files straight through (world_session/mod.rs:1205-1212 maps Vec<CompileFile> into CompileSpec with no dedup), so any admin client that assembles a file list programmatically (or a buggy/duplicate-emitting client) can trigger this. Compilation is admin-only, which bounds the blast radius, but the outcome is either a confusing refusal, a silently different module than the admin sent, or — worst — an installed module whose recorded source provenance does not match the sources it was built from.

Evidence

Verified on main 54f862c9 with a probe crate on the laptop (~/git/fleet/probe-2026-10-27-laneB-dupcompile there) driving verbatim copies of check_imports, validate_spec, digest_sources, and sanitize_file_name from world_compile.rs (sha256 8a61af784d1698d96ef1d58ea5341432ebb82dc72fe2cb4053acca460cc3b580), with the staging loop reproduced exactly (std::fs::write standing in for tokio::fs::write, same path and order). Log: hidden_files/probe-2026-10-27-laneB-dupcompile/out.log in the goal workspace.

CTRL validate=Ok staged=[("Helper.roc", "HELPER"), ("main.roc", "MAIN")] digest_match=true
DUP1 validate=Ok staged=[("Helper.roc", "HELPER-SECOND"), ("main.roc", "MAIN")]
DUP1 digest(spec)=d30c2696ed39ad9f digest(staged)=4c07d9907fddb96e match=false
DUP2 validate=Ok files[0]="MAIN-FIRST" disk_main="MAIN-SECOND" staged=[("Helper.roc", "HELPER"), ("main.roc", "MAIN-SECOND")]
DUP3 check_imports=Err Helper.roc imports Chat, which this world does not allow (grant it with `id world caps --grant Chat`; …)
DUP3 staged=[("Helper.roc", "clean\n"), ("main.roc", "MAIN")] (the importing file is not among them)
  • CTRL: unique names validate, stage loss-free, and digest(spec) == digest(staged).
  • DUP1: [main.roc, Helper.roc="HELPER-FIRST", Helper.roc="HELPER-SECOND"] validates; the first Helper.roc is silently lost at staging; the digest recorded as provenance (digest(spec)) differs from the digest of the sources actually compiled (digest(staged)).
  • DUP2: [main.roc="MAIN-FIRST", Helper.roc, main.roc="MAIN-SECOND"] validates on the strength of files[0], but the main.roc read back from the job dir — the one whose platform path is rewritten and which roc build main.roc compiles — is MAIN-SECOND.
  • DUP3: [main.roc, Helper.roc="import Chat\n", Helper.roc="clean\n"] is refused by check_imports for an import in a file that is shadowed and never compiled.

Why it matters

ModuleProvenance is the record of what a running world module was built from; id world verify/doctor-style audits and incident response lean on source_blake3 meaning "these exact sources". A duplicate name quietly breaks that invariant while also discarding one of the admin's files without any error. Every sibling validator in this area refuses out-of-contract input by name (unsafe names, oversize files, ungranted imports); duplicate names are the one contract hole left.

Acceptance criteria

  • validate_spec refuses a spec with duplicate file names, with an error that names the duplicated file (so compile(), which validates before imports/digest/staging at :202-203, refuses before any stage can diverge).
  • Regression tests in world_compile.rs cover at minimum: a duplicated non-main file, a duplicated main.roc, and that a unique-name spec (control) still validates.
  • Existing behaviour is unchanged for unique-name specs: the current world_compile test module keeps passing, including the source-digest order-independence test.
  • No change to the digest format, the staging layout, or the provenance schema — refusal at validation is the whole fix surface.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    on Oct 28, 2026
  2. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  3. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 28, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: in-progress (guarded self-solve, lifecycle protocol §4)

    • Agent: new-issue-creator-code-b
    • Session / ID: new-issue-creator-code-b cron run, 2026-10-27 23:50 CDT
    • Found via: filed this run
    • Task: Self-solving this run — the fix is a single validation refusal in validate_spec (pkgs/id/src/world_compile.rs), trivially scoped: refuse duplicate file names with an error naming the duplicated file, plus regression tests (duplicated helper, duplicated main.roc, unique-name control). compile() validates before check_imports/digest/staging, so this one check closes all three divergences (silent overwrite, provenance digest mismatch, import scan over shadowed files). No digest-format, staging-layout, or provenance-schema change.
    • Expectation: effort/xs — branch creator-b/1593-compile-duplicate-names, targeted world_compile tests on the laptop, PR + merge when verified.
    • Links: probe log in the goal's hidden_files/probe-2026-10-27-laneB-dupcompile/out.log (main 54f862c; CTRL digest_match=true, DUP1 validate Ok + digest mismatch, DUP2 disk main = second main.roc, DUP3 refusal from a shadowed file)
  5. added a commit that references this issue on Oct 28, 2026
  6. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: closed (self-solved and merged this run)

    • Agent: new-issue-creator-code-b
    • Result: PR fix(id): refuse duplicate file names in world compile requests (#1593) #1594 merged (merge 6516519, fix 1b4812b). validate_spec (pkgs/id/src/world_compile.rs) now refuses a compile request with duplicate file names, naming the duplicated file; regression test duplicate_file_names_are_refused covers a duplicated helper, a duplicated main.roc, and a unique-name control.
    • Verified: real-crate cargo test --lib world_compile on the laptop: 21 passed / 0 failed (20 pre-existing + new test); cargo fmt --check clean; probe re-run with the patched validator flips DUP1/DUP2 to refusals naming the file, control unchanged (goal hidden_files/probe-2026-10-27-laneB-dupcompile/out-fixed.log). PR CI was still pending at merge; merged under the standing directive on local verification.
    • Found via: filed this run (probe log: hidden_files/probe-2026-10-27-laneB-dupcompile/out.log)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobstatus:done

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions