Skip to content

id(world): check_schedule_replacing trusts a caller-supplied replacing flag — a mismatched flag admits a 65th pending schedule past the cap restore enforces, and underflows on an empty map #2882

Closed

Description

@dezren39

Summary

WorldCore::check_schedule_replacing (pkgs/id/src/world/core.rs:600-627) trusts a caller-supplied replacing: bool to discount the pending-schedule count, instead of checking its own map:

let pending = self.schedules.len() - usize::from(replacing);   // core.rs:612
ensure!(pending < crate::world_caps::MAX_PENDING_SCHEDULES, ...); // 64, world_caps.rs:95

The flag is redundant — whether the id being set is actually pending is knowable from self.schedules — and a mismatched flag breaks the cap in both directions:

  1. Full map, absent id, replacing=true: with 64 schedules pending, pending computes as 63, the check passes, and the caller inserts a 65th pending schedule — one more than MAX_PENDING_SCHEDULES (64). That state is one journal replay refuses: restore rejects a journal whose pending ScheduleSet entries reach the cap (world/core.rs:263-275, core.schedules.len() < MAX_PENDING_SCHEDULES per insert), so a journal ever written in that state would make the world unopenable.
  2. Empty map, replacing=true: 0 - 1 on usize panics in debug builds (arithmetic overflow) and wraps to usize::MAX in release, making the check refuse a schedule that should be admitted.

Evidence (main b4136991, code-read verified this run)

  • pkgs/id/src/world/core.rs:612 — the subtraction above; the duplicate check at :607-610 is keyed on the same flag (replacing || !contains_key), so the flag controls both guards while the map, which knows the truth, is consulted for neither.
  • pkgs/id/src/world_caps.rs:95 — MAX_PENDING_SCHEDULES: usize = 64.
  • pkgs/id/src/world/core.rs:271-275 — journal restore enforces the same cap per ScheduleSet, i.e. the cap is a hard invariant of persisted state, not an advisory limit.
  • Sole production caller, pkgs/id/src/world/actor.rs:1376-1383 (WorldCommand::AdminScheduleAt), currently computes the flag correctly (let replacing = self.core.schedules.contains_key(&id);) — so today this is a latent trap, not a live breach: the invariant holds only by caller discipline, in exactly the function whose job is to enforce it.
  • Coverage gap: pkgs/id/src/world/tests.rs:3405-3444 (a_replacement_is_validated_as_a_replacement_not_as_an_addition) exercises replacing=true only with the id actually pending ("taken", "s0"). No test passes a mismatched flag.

Verification note: verified by reading the code at the lines above and tracing the arithmetic for the boundary cases (64 pending + absent id + true; 0 pending + true). No compiled probe this run: the laptop SSH tunnel is down (see #2879), and the sandbox does not build. The boundary trace is deterministic from the quoted lines; triage can confirm with the two unit cases in the acceptance criteria in seconds.

Why it matters

The pending-schedule cap bounds journal size and wakeup work per world, and restore treats it as a hard invariant. An enforcement helper that can be talked out of the cap — or into a panic — by a redundant boolean is a footgun for the next caller (the manifest-apply path already grew one new caller in #807; SetSchedule frames and program schedule.at are adjacent surfaces). Deriving the discount from the map removes the whole class.

Acceptance criteria

  • check_schedule_replacing derives whether the id is being replaced from self.schedules itself (or otherwise cannot be desynchronised from the map); no caller-supplied flag can discount a schedule that is not pending.
  • Unit test: 64 schedules pending + set of a new id through the replacement path is refused with the "pending schedules" error.
  • Unit test: replacement path on an empty map (and on a map not containing the id) neither panics nor wraps.
  • Existing tests still pass, including a_replacement_is_validated_as_a_replacement_not_as_an_addition (a true replacement at capacity still fits) and the id: F12.1 apply wiring — module install + schedules reconciliation #807 guarantee that a refused replacement leaves the existing schedule standing.
  • The duplicate guard and the capacity discount agree: both keyed on the same map lookup.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-30 19:05 CDT (2026-10-31T00:22Z)
    • Found via: filed this run
    • Task: Triage the check_schedule_replacing finding: world/core.rs:612 discounts the pending count by a caller-supplied replacing bool instead of its own map — full map + absent id + true admits a 65th schedule past the MAX_PENDING_SCHEDULES cap restore enforces (core.rs:271), and empty map + true underflows usize. Sole caller (actor.rs:1376) computes the flag correctly today, so this is a latent trap in the enforcing function. Verified by code-read + boundary trace on main b413699; no compiled probe (laptop SSH down, fleet: laptop SSH unreachable again 2026-10-30 — MaxStartups drops blocking conductor + lanes (recurrence of #1667) #2879).
    • Expectation: effort/xs — derive the discount from contains_key inside the helper + the two boundary unit tests; done when a mismatched flag can neither admit past the cap nor panic.
    • Links: evidence file hidden_files/issue-code-schedule-replacing-cap-body.md
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Agent note (design-architect, 2026-10-30): this issue is Phase 3 of design #2898 ("reconstruction is admission", design doc files/designs/code-id-reconstruction-validation.md in the keep-the-four-repos-healthy goal workspace), together with #2876 and the closing reconstruction-seam audit.

    Recorded decisions for triage — do not re-file, re-scope, or split this issue; record the phase structure instead:

    • Derive, don't trust: check_schedule_replacing loses its caller-supplied replacing: bool; whether the id replaces a pending schedule is derived from the core's own schedules map inside the check, so live admission and restore enforce the identical cap from the identical ground truth. The crate-private wrapper and the actor call site stop supplying the fact (passing state the same function already computed from its own map is acceptable only if the check itself re-derives it).
    • The empty-map underflow disappears with the trusted flag; keep a regression probe for both probes in this issue (65th schedule; empty-map replace).
  4. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 74 remaining items

  6. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — holder: repo-worker-code (OpenChamber scheduled lane), at 2026-11-02T14:46:48Z. event: picked-up; task: 5457; queue: repo-health-code-pilot. Fresh metadata-only revalidation: issue #2882 OPEN, assigned dezren39; canonical PR #4261 OPEN, non-draft, authored and assigned to dezren39. Existing checkpoint identifies this as a Drew-owned hard stop. No checks, diff, reviews, branch, worktree, or source inspected or touched.

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — holder: repo-worker-code (OpenChamber scheduled lane), at 2026-11-02T14:47:21Z. event: parked; task: 5457; queue: repo-health-code-pilot. Parked 30 minutes for the explicit ownership release/PR disposition condition; preserve existing branch/worktree and checkpoint, do not inspect or touch the Drew-owned PR absent release.

  9. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  10. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  11. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions