Skip to content

objectql: deleting a record does not release the sys_secret row its secret field names #27581

Closed

Description

@objectstack-fleet

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

Filing gate: ① a product defect with a reproduction. Found by the #27495 card thread while implementing PR #27572, and measured at the public data door. reach: the REST data door, where a DELETE answers 200 and the record's sys_secret row stays.
Business reason: deleting a record that holds a secret value keeps its encrypted credential row at rest, with nothing pointing at it. The user deleted the record and believes the credential is gone, but the ciphertext is still stored and decryptable, and the credential store grows with every such delete. #27495 (PR #27572) closes the same leak for refused writes and for clearing or replacing the value. This card is the delete verb, which that card does not cover.
Reader who acts: the triage seat, then the implementer for the engine's delete path (packages/objectql, the engine.ts delete flow, beside the verified release step releaseSecretHandles that #27572 adds in engine-secret-internal-fields.ts).
Dedupe (open AND closed): the query "delete record secret field sys_secret row" returned 2 hits, none the same.

Back-link: #27495 · ⛔ Not a claim. Grading is triage's.

Symptom

Deleting a record whose secret field holds a value leaves behind the sys_secret row that field named.

Reproduction (head c0b6bfc7 of PR #27572, a booted dogfood stack, the REST data door)

  1. On an object with a secret field token, POST /data/OBJECT {"name":"a","token":"value-a"} → 201. The stored token is secret:HANDLE, and sys_secret holds N rows, one of them with id HANDLE.
  2. DELETE /data/OBJECT/ID → 200.
  3. sys_secret still holds N rows, the row with id HANDLE among them, and no record names it.

Expected: a committed delete releases the credential row the deleted record's secret field named. It should use the verification #27572 uses: no remaining row of that column names the handle, the handle is that column's own, and the delete rides the record's transaction. When that cannot be confirmed, it keeps the row and logs one line.

Mechanism

The delete path never reads the deleted row's secret columns, so nothing releases their handles. Orphans already at rest stay report-only through os secret orphans, per #8259.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 业务对象与记录 | records-forms.encrypted-field-behavior | P②

    Triage: p1 · domain:server · area:records · 2026-10-29T14:08Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    priority:p1High: required for production / M2
    and removed on Oct 29, 2026
  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    Claim: PM loop round 2 · card thread
    Session: session_01L6bhgUCizoYWQVJWjhdR6F (the seat); the card thread is session_01L6bhgUFEohazYphk3zDAum
    Account: os-sales
    Branch: claude/project-thread-v9mp90 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: objectstack-issue-27581 in the card thread's own container
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/objectql/src/engine.ts (hot file; region = the delete flow only), packages/objectql/src/engine-secret-internal-fields.ts (reuse of releaseSecretHandles), and tests (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the objectql delete path (this lane) | #27572's release step covers refused writes and clear/replace, not delete; os secret orphans is report-only | every caller that deletes a record with a secret field
    Thread-read: 6498922040
    Premise: holds (read on main 2026-10-29T15:20Z: PR #27572 is merged; the delete flow reads no secret column of the deleted row)
    Serial constraints cleared: engine.ts — seat 1's queue names #25522 (pm:queue, no cut claimed); the thread runs git merge-tree against every open PR on the file before its push. engine-secret-internal-fields.ts — #27582 (this seat, encrypt path, a different region) is dispatched after this claim; the later lander merges main

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
    os-dev-report

    {
      "issue": 27581,
      "status": "done",
      "branch": "claude/project-thread-v9mp90",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27657",
      "session": "session_01L6bhgUFEohazYphk3zDAum (the card thread; the seat is session_01L6bhgUCizoYWQVJWjhdR6F)",
      "premise_still_valid": true,
      "summary": "Premise measured on main 9c513147: the delete flow never read the deleted rows' secret columns, so no handle was released. A committed delete now hands the existing releaseSecretHandles (the 'retired' occasion #27572 added for a null clear) every handle the deleted rows held: deletedSecretSlots in engine-secret-internal-fields.ts is retiredSecretSlots with a null clear of every secret column, so there is one release rule. Candidates come from reads the delete already makes: the by-id pre-image; on a predicate delete with no cascade plan the per-row hook read, else one narrow id + secret-columns read before the statement; on a cascade plan the matched-row read inside the unit carries the secret columns. Cascade members go through the by-id path and release their own. The release runs after the afterDelete chain and its result-shape check, before the roll-up, so a refused or rolled-back delete keeps the row. Delete paths that exist: by id, predicate on three plans (none, atomic, split), cascade members; the REST DELETE, the deleteMany door and the cross-object batch delete all reach the engine delete by id. No soft-delete or recycle-bin path exists (softDelete and enable.trash were retired from the spec in 16.x), so no kept-record case. engine.ts region: the delete flow, two names in the secret-fields import list, one docblock sentence on releaseSecretHandles. Head b15352e9 (origin/main f7ff693c merged once); the card's assignee and labels were not touched.",
      "tests": "Red first on the unfixed tree (engine.ts and engine-secret-internal-fields.ts at base 9c513147, new tests kept): secret-field-write-commit.test.ts 6 failed / 24 passed (30); the 3 keep cases were green there. At b15352e9: pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=3: Test Files 488 passed (488), Tests 9163 passed (9163); objectql typecheck exit 0 (check:test-typecheck OK, 39 files / 233 errors / 64 pinned signatures, unchanged); dogfood typecheck exit 0; dogfood secret-field-delete-release + secret-field-orphan-rows: 2 files, 9 passed (9); eslint on the four changed files exit 0; pnpm lint (repo-wide) exit 0. Ablation through scripts/ablation-replace.mjs at 54efb0b8 (fix committed), each anchor 1 to 0, each restore equals the HEAD blob (engine.ts ab2b884a7373, engine-secret-internal-fields.ts 621c5ec3c95f) with git diff HEAD empty. Source legs: A1 release call removed 6 failed / 24 passed; A2 shared-handle check removed 2 failed (27495 case + the delete case); A3 store options stripped of the unit 1 failed (the rolled-back delete on the double's isolation mode); A4 ownership check removed 2 failed. Dist legs (marker found in 4 dist files by ablation-dist-preflight before each run): D1 release removed 3 failed / 1 passed; D2 shared-handle check removed 1 failed; D3 unit stripped 1 failed, the atomic deleteMany rollback, by test timeout 5000ms (the release outside the unit waits on the single sqlite-wasm connection the batch holds). Restore: rebuilt with declarations, --absent exit 0 for all three markers, the file 4 passed (4), tree clean.",
      "gates": "dispatch-gates --commands derived 67 at b15352e9; all 67 run, exit captured before any pipe: 67 x exit 0; --ran: 67 derived, 67 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN. First pass: check:dual-build-cjs-loads exit 3 for 7 missing dists (built, then exit 0). Overlap: git merge-tree against every open PR touching the changed files, measured over every open PR: only #27477 (head 5eb62f09, engine.ts), clean; clean with current origin/main. Seat 1 hot-file queue on #6367 names #25522 then #27165 for engine.ts; neither has an open PR. CI on PR check-runs at report time: 22 check-runs, 3 skipped by path (Schedule gate, 18.x upgrade harness, Packed-tarball smoke), the rest in_progress; none concluded yet.",
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "10 GitHub MCP, reads only: issue_read x5, pull_request_read x2, list_pull_requests x3; plus subscribe_pr_activity on the PR (session tooling, not a GitHub write); no MCP write tool",
      "api_writes": "2 relay writes as objectstack-fleet[bot]: pr_create (run 40912381966, read back 11606 of 11606 bytes identical, draft, no assignees, no reviewers) and this comment via post-stamped. git push to the branch is not a REST write.",
      "deviations": [
        "PR body attribution: the header links this card thread and the footer names this card thread's session, as the session's own PR-attribution requirement states; order-common item 6 names the seat 2 thread and the seat session. The relay stamped the same session. The seat may rewrite the header if it wants the seat link.",
        "The dispatch asked for pnpm lint; os-dev counts the repo-wide lint as CI's. It was run anyway at the final head: exit 0.",
        "The REST rollback pin's tx ablation goes red by timeout, not by assertion; the assertion-level red is the engine case on the double's new isolation mode. Declared in the PR.",
        "The existing double was extended (deleteMany, an isolation rollback mode) instead of pinning a new one; check:objectql-double-limit and check:engine-double-contract stay green."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }
  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    ACCEPT → PR #27657 comment 6503022792 · head b15352e9 · 2026-10-29T16:57Z · landing sent.

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions