Skip to content

storage: the chunk door accepts chunk indexes outside the declared range and answers 500 on a negative index, leaving a session that can never complete #27412

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

QA-source: #27409 · attachments-storage.chunked-upload-integrity · observed beside the clauses

Found by an off-clause probe (chunk-index boundary values) in the 18.3.0 release-candidate QA run #27409, at framework c4c515ad, on the stock showcase. Signed in through the form, so cookie and bearer were both present.

Reproduction

  1. As admin, POST /api/v1/storage/upload/chunked with {"filename":"b.bin","mimeType":"application/octet-stream","totalSize":100}. The answer gives totalChunks: 1 and a resumeToken.
  2. PUT /api/v1/storage/upload/chunked/UPLOAD_ID/chunk/-1 with header x-resume-token and a 10-byte body. It answers 500 INTERNAL.
  3. PUT .../chunk/1 and PUT .../chunk/7 with the same header. Each answers 200. The progress door then reports uploadedChunks: 2 of totalChunks: 1, percentComplete: 20.
  4. PUT .../chunk/0 with all 100 bytes, then POST .../complete with {"parts":[{"chunkIndex":0,"eTag":"ETAG"}]}. It answers 409: "holds chunks 1, 7, beyond the 1 it declared".
  5. No door removes a stray chunk, so the session can only expire. A non-integer index such as 0.5 is read as 0.

Expected vs actual

  • Expected: 400 INVALID_REQUEST for a chunkIndex that is not an integer in [0, totalChunks). Nothing is stored and nothing is counted in progress.
  • Actual: -1 answers 500. An out-of-range index answers 200, is stored, is counted in progress, and then permanently blocks completion.

Mechanism

  • packages/services/service-storage/src/storage-routes.ts (around lines 1162 to 1167) parses the index with parseInt and checks only isNaN. There is no range check against the session's total_chunks, and no integer check.
  • -1 reaches storage.uploadChunk(uploadId, chunkIndex + 1) as part number 0 (around line 1233), which throws, and the catch maps that to 500.

Suggested acceptance

  • -1, totalChunks, 7 and 0.5 each answer 400 naming the valid range.
  • The session's parts list and progress are unchanged after those refusals, and a following in-range upload completes.
  • A pin is added beside chunked-upload-integrity.test.ts.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 文件 | none | P②

    Triage: p2 · domain:server · area:files · 2026-10-29T06:01Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    area:filesFiles — upload, download, signed URLs, access derived from the parent record
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 29, 2026
  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Path: 文件 | none | P②

    Triage: lands in packages/services/service-storage/src/storage-routes.ts (domain:server); rationale: this concurs with the first touch at 06:02Z (comment 6489268755). Labels stay as they are (bug, priority:p2, pm:queue, area:files, domain:server). This note adds the parts of the first touch that the duties require and that were missing. Read at origin/main ded67bfb, 2026-10-29T09:55Z.

    Fold suggestion (the execution seat decides): fold with #27413. Both are input validation on route params in the same file, storage-routes.ts, at the presign door (about line 916) and the chunk door (about line 1162). One PR avoids two serial claims on one hot file. Draft PR #27424 (sys_attachment hardening, head bc7274ef) is in flight in the same package. Its file list does not include storage-routes.ts, so the two are disjoint, and neither needs to wait for the other.

    Size/model suggestion: S each, or S/M folded.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Claim: PM loop round 1
    Session: session_017xRBcScm27Q3R5mkYLPWdB
    Account: sunhaolin
    Branch: claude/issue-27412-storage-route-input
    Worktree: objectstack-issue-27412
    Domain: domain:server
    Seat: domain:devx#1
    File surface: packages/services/service-storage/src/storage-routes.ts (the presign door around the size check and the chunk door around the chunkIndex parse only), packages/services/service-storage/src/storage-routes.test.ts or a sibling test, one @objectstack/service-storage changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default (dispatch-gates --tier at c4c515a: "no path-derived mandate"), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: service-storage route code (storage-routes.ts) parses chunkIndex and size without range/type checks | no platform path covers it; the request-validation sibling doors already answer 400 for malformed input | reached by any signed-in uploader; the console upload client never sends these values, so no legitimate caller relies on them
    Thread-read: 6494062263
    Premise: holds (triage first touch re-verified the mechanism on origin/main ded67bfb; the dev re-measures before editing)
    Serial constraints cleared: none — the 10 open domain:server + pm:dispatched claims were read, none names service-storage; draft PR #27424 (service-storage) does not touch storage-routes.ts (its file list was read)

    Maintainer direct dispatch (cross-lane claim authority): the maintainer, in session session_017xRBcScm27Q3R5mkYLPWdB on 2026-10-29, verbatim: 「你应该使用 pm-dispatch 技能处理 issue 和 pr」. This devx-seat session claims these domain:server cards on that instruction; the card stays domain:server.
    Fold: this card is the chain head of a two-card family dispatch with #27413 (one branch, one PR, one Fixes line per card, one commit per card). Five fold gates: ① same defect form and fix: a storage route accepts a malformed request parameter (an out-of-range/non-integer chunkIndex; a non-numeric or negative size) and answers 500 or stores it, and the fix is a 400 refusal at the door; ② same package and file; ③ both triaged (p2); ④ each independently checkable: chunk indexes -1, 0.5, totalChunks answer 400, and size "abc", -5 answer 400; ⑤ excluded: draft PR #27424 (same package, different files and mechanism). Clause-② note: refusing inputs the routes accepted narrows a published request accept set, so it is no and owes one contract-review-tier review before the queue.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
    os-dev-report

    {
      "issue": 27412,
      "folded": [
        27413
      ],
      "status": "done",
      "branch": "claude/issue-27412-storage-route-input",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27537",
      "session": "session_017xRBcScm27Q3R5mkYLPWdB",
      "premise_still_valid": true,
      "summary": "Both mechanisms re-measured on origin/main ded67bfb and held. Chunk door (#27412): chunkIndex is now read strictly (decimal digits, safe integer) and judged against the session's total_chunks after the resume-token/organization/expiry checks; anything outside [0, totalChunks) answers 400 INVALID_REQUEST naming the range with details {chunkIndex, totalChunks}, nothing stored or counted. Presigned door (#27413): size must be a finite non-negative JSON number, else 400 INVALID_REQUEST naming size, before the pending sys_file row. No new error code; INVALID_REQUEST is the code these doors already answer. PR #27424 re-checked: open draft at bc7274ef, its file list does not contain storage-routes.ts; origin/main merged (3604371f) before the PR opened.",
      "tests": "At dbfaf81c: pnpm --filter @objectstack/service-storage test -> Test Files 70 passed (70), Tests 1278 passed (1278); typecheck (tsc + scripts + check:test-typecheck) OK; lock VERDICT command-exit 0. First run before the storage-limits fixture update: Tests 2 failed | 1276 passed (two limit cases sent chunk index 1 to a one-chunk upload). New pins: 7 refused chunk indexes + 3 controls (chunked-upload-integrity.test.ts), 8 refused sizes + 2 controls (storage-routes.test.ts), each asserting status and error.code. Ablation via scripts/ablation-replace.mjs (tests import src, no dist): range check removed -> 3 failed | 20 passed; size check removed -> 8 failed | 52 passed; both restored blob == HEAD 6c89ee52a759, git diff HEAD empty. Merge of origin/main (3604371f) touches no service-storage file; package suite not rerun after it.",
      "gates": "Local: dispatch-gates --commands at 3a10b29c derived 64; dispatch-gates --ran: 64 accounted, 61 run exit 0, 3 NOT-MEASURED exit 3 (pnpm check:dual-build-cjs-loads: workspace dists not built; pnpm check:i18n: CLI not built; node scripts/check-plugin-teardown-shape.mjs --self-test: shallow clone, pinned fixture commit unreachable). check-nul-bytes: OK. check-platform-checklist not owed (no export renamed or removed). CI on PR head 3a10b29c at report time: Governed Surface Queue Guard success; Check Changeset, Lint lanes, Type Check lanes, PR-body guards in_progress; Test Core / Build Core not yet listed.",
      "line_budget": "200 changed lines (+195 / -5), 5 files, vs the 3000 human-merge threshold: under. No skills/** touched.",
      "files_changed": [
        ".changeset/27412-storage-route-input.md",
        "packages/services/service-storage/src/storage-routes.ts",
        "packages/services/service-storage/src/chunked-upload-integrity.test.ts",
        "packages/services/service-storage/src/storage-routes.test.ts",
        "packages/services/service-storage/src/storage-limits.test.ts"
      ],
      "deviations": [
        "File surface: storage-limits.test.ts edited (two upload-limit cases sent chunk index 1 to a one-chunk upload; they now widen the session row to two declared chunks). Read as 'a sibling test' in the surface; it is a stale fixture the #27412 refusal made red, fixed in this PR.",
        "One commit per card: three commits. 88c6f71d (#27412), 48b95c96 (#27413), dbfaf81c (the storage-limits fixture, a #27412 consequence). Folding it into 88c6f71d needed reset + force-with-lease; the auto-mode classifier refused that command ([Git Destructive]), so it was not retried another way.",
        "Commit trailers use the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md, not the harness reminder's model-named Co-Authored-By line.",
        "#27413's claim names worktree objectstack-issue-27413; the dispatch and the #27412 claim name objectstack-issue-27412, which is what was used.",
        "Package suite was not rerun on 3a10b29c (the merge of origin/main touches no service-storage file).",
        "Report comment: the os-dev-report marker is the SECOND line; post-stamped.mjs refuses a body whose first line is not the seat signature, and the check-half-states marker pattern accepts one signature line before it."
      ],
      "dogfood_battery": "n/a: no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 relay strokes: (1) fleet-write pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/27537/assignees [sunhaolin], read back identical (7733 bytes); (2) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/27412/comments. git push not counted. Reads: gh api GET on issues 27412/27413 + comments, pulls/27424/files, pulls/27537, check-runs.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: GetPresignedUrlRequestSchema.size (packages/spec/src/api/storage.zod.ts) is z.number() with no .int() or .min(0), so the spec still declares negative and fractional sizes legal while the presigned door now refuses negatives. No producer sends either; it is in the PR's Acceptance notes. Dedupe words: presigned size spec int min, GetPresignedUrlRequestSchema size, storage upload size contract"
      ]
    }
  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
    os-dev-report

    {
      "issue": 27412,
      "folded": [27413],
      "status": "blocked",
      "round": "patch: seat REWORK at 3a10b29c (Clause-② narrowing arm)",
      "branch": "claude/issue-27412-storage-route-input",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27537",
      "session": "session_017xRBcScm27Q3R5mkYLPWdB",
      "head": "41272f9f5693500d15a712731557f41502a8d982",
      "summary": "Changeset is done. PR body edit is not done: the relay refused it. Two new commits, no amend and no force-push. 1ee2b1e9 sets the changeset's line to `Clause-②: no (narrowing)` and adds the sentence: 'Narrowed: a negative `size`, which the spec number (`GetPresignedUrlRequestSchema.size`, a bare `z.number()`) admits, is now refused `400` at the presigned door.' 41272f9f adds the ADR-0087 disposition that the (narrowing) arm now requires: `adr-0087: not-required (no-migration-prescription)`, in the HTML-comment form that the gate prints and the existing changesets use. Without it, check-adr-0087-registration exits 1 for this changeset. The PR body edit (op issue_patch on 27537) was refused by the relay itself, not by the classifier: `fleet-write/dispatch: REFUSED — action 1 (issue_patch)'s body opens with no signature line`. SIGNED_EDIT_OPS = comment_edit and issue_patch require the seat signature as the first line, and this body must open with `Fixes #27412`. Per the instruction I did not retry another way, did not prepend a signature line, and did not use MCP. One more point: os-dev forbids a dev PATCH of the PR body ('事后要改的报告点名改法,席位代写').",
      "pr_body_edit_owed": "Exact diff against the live body, read from GET pulls/27537 and otherwise byte-identical. Line 4 `Clause-②: no` becomes `Clause-②: no (narrowing)`. After line 5 (`Metadata-protocol: none`) insert one blank line, then: Narrowed: a negative `size`, which the spec number (`GetPresignedUrlRequestSchema.size`, a bare `z.number()`) admits, is now refused `400` at the presigned door. The full prepared body is at /tmp/claude-0/-home-user-objectstack/e667d31e-a318-53f2-a4bf-0340b5d89e6a/scratchpad/issue-27412/pr-body2.md. The refused command: node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file /tmp/claude-0/-home-user-objectstack/e667d31e-a318-53f2-a4bf-0340b5d89e6a/scratchpad/issue-27412/patch-actions.json --session session_017xRBcScm27Q3R5mkYLPWdB --dry-run (exit 2; nothing sent). The body's Acceptance note 'three commits, not two' is stale now that there are five commits; I left it, as instructed.",
      "readback": "Changeset: GET contents/.changeset/27412-storage-route-input.md?ref=41272f9f shows `Clause-②: no (narrowing)`, the adr-0087 not-required (no-migration-prescription) marker and the Narrowed sentence. PR body: not edited; live body still reads `Clause-②: no`.",
      "tests": "Not rerun: the diff since 3a10b29c is the changeset only.",
      "gates": "On the final changeset, local: node scripts/check-adr-0087-registration.mjs --base origin/main exit 0, '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'; node scripts/check-changeset-no-major.mjs --base origin/main exit 0 (level axis not applicable without a PR payload); node scripts/check-empty-changeset.mjs --base origin/main exit 0; control-byte scan of the changeset: 0 hits. CI has not been read on 41272f9f.",
      "dogfood_battery": "Owed by the (narrowing) arm. 41272f9f: lock VERDICT command-exit 0; Test Files 348 passed | 3 skipped (351); Tests 2806 passed | 20 skipped (2826); no failures.",
      "line_budget": "Changeset grew by 4 lines (marker, sentence and blank lines). Code and tests unchanged.",
      "files_changed": [".changeset/27412-storage-route-input.md"],
      "deviations": [
        "One extra commit beyond the asked changeset edit: the ADR-0087 disposition marker, which the (narrowing) arm makes mandatory (check-adr-0087-registration red without it). The marker is an HTML comment inside a repo changeset file, in the gate's own required form. It is not GitHub text.",
        "PR body edit not sent: the relay refused issue_patch without a leading signature line. Stopped as instructed."
      ],
      "mcp_calls": "0",
      "api_writes": "1: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/27412/comments via post-stamped/relay. The PR body issue_patch was refused at --dry-run, so nothing was sent. git push is not counted.",
      "open_questions": [],
      "out_of_scope_findings": []
    }
  7. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    ACCEPT → PR #27537 (head 41272f9f, contract review PASS 6496658225), reviewed 2026-10-29T12:29Z. Ready to land, held on the landing permission. Follow-up for the sibling init door and the spec size: #27549.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Handover: PR #27537 → PM session session_017xRBcVy4VXmkQjn2MGeaWk (review and landing)


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:filesFiles — upload, download, signed URLs, access derived from the parent recordbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions