Repository navigation
事务句柄没有属主可查,同源校验对「引擎没开过的句柄」只能弃权(#5351 的已知边界) #6167
Description
Activity
Findings triage (#4949 discipline): hold —
findingretained,domain:engine-coreretained.Stale-premise check (
origin/main@42af12f). The boundary is real and — unlike its sibling #6168, promoted this round — it is declared:packages/objectql/src/engine.ts:950–:961: the txStorescopefield's own doc says the owner is "Absent on the sandbox runner's explicitly-threaded handles (thebeginTransaction/commit/rollbacktrio does not use this store at all) and on any store entry an outside caller populated, so every reader must treat it as optional." The finding is describing something the source already says about itself.- The trio is at
:7354–:7373; it callsdriver.beginTransaction()directly and never touches txStore, matching the finding exactly. ⚠️ One premise is not on main yet. The pin the issue cites as fixing this boundary in place —engine-transaction-same-origin.test.ts's "does NOT cover a foreign handle passed in from outside — declared, not overlooked" — arrives with PR fix(objectql)!: 事务句柄不再跨数据源穿透 —— 业务写响亮拒绝、系统账本移出事务落盘 (#5351) #6171 (that file is+508/−0in it; open draft, stacked on feat(spec,objectql): transaction 契约收紧第一批 ——opts.requirefail-closed 与owned信号 (#5696) #6165). The ADR-0067 / ADR-0119 revisions are in the same PR. The verdict is unchanged either way, but the next round should re-read this if fix(objectql)!: 事务句柄不再跨数据源穿透 —— 业务写响亮拒绝、系统账本移出事务落盘 (#5351) #6171 changes shape.
Why hold, rather than queue or escalate:
- Nothing user-reachable today. It needs a sandbox hook opening through the discrete trio and a write inside it routed to a second datasource. The fail-direction is abstention — the check keeps pre-ambient 事务句柄跨数据源泄漏:凡在事务中执行的被审计写入,合规审计行全部静默丢失(#5226 的真实根因) #5351 behaviour rather than guessing.
- Closing it is an
IDataDrivercontract change, and the body offers three shapes with materially different blast radii: an optionalownsTransaction?(handle)predicate; an engine-side envelope around everybeginTransactionreturn, which reaches each driver's.transacting(trx)call surface; or a non-ALS owner table that only solves path 1. Picking one is a contract decision with no consumer pull behind it, and the body's own argument is the strongest case for waiting: 「一个保证建立在猜测之上,比明确不覆盖更坏。」 - Not
needs-user-decision. That label means a decision is pending in the maintainer's inbox. Nothing is waiting on them here — the decision was made (abstain, and declare the abstention) and is shipping in fix(objectql)!: 事务句柄不再跨数据源穿透 —— 业务写响亮拒绝、系统账本移出事务落盘 (#5351) #6171. Escalating now would file a question nobody is blocked on, which is the state the label discipline exists to keep out of the inbox.
Restart conditions — promote on any one:
- (a) ScopedContext.transaction 不 join ambient 事务 —— 沙箱 hook 体里的 ctx.api.transaction() 会在事务中再开一个事务(违反 ADR-0067 D2) #6168's join fix (queued this round) turns out to need an ownership discriminator after all — i.e. the trio's handles can reach txStore, falsifying the
:958doc. That measurement is an explicit item in ScopedContext.transaction 不 join ambient 事务 —— 沙箱 hook 体里的 ctx.api.transaction() 会在事务中再开一个事务(违反 ADR-0067 D2) #6168's dispatch, so the answer arrives on its own without anyone re-deriving it here. - (b) A deployment routes a sandbox-opened transaction to a second datasource — the abstention becomes reachable, and this promotes at whatever severity the missed carve-out earns.
- (c) The
IDataDriversurface is reopened for another reason — the predicate rides along instead of paying for a contract change of its own.
Cross-seat note, recorded rather than acted on: whichever shape wins, options 1 and 2 touch
IDataDriverinpackages/spec⇒ under "shared contract surfaces have one owner" the contract half transfers to thedomain:specseat and the engine half stays indomain:engine-core. ⛔ Not splitting today — there is nothing to sequence until an appetite exists, and aBlocked-by:chain with no upstream is just a state nobody can exit.Serialization:
engine.tscarries the two stacked open drafts above (#6165, #6171). Recorded for the next reader; not a blocker, since nothing is being dispatched.Dedup: three-repo scan of open issues and PRs — #5351 / #5696 / #5724 / #4619 are the parents and are cited in the body; #6168 is the sibling. No independent duplicate.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsFindings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): closing, not planned. By the body's own record, this boundary was written into the ADR-0067/0119 revisions and pinned in tests as a decision, not a gap — an accepted, documented limit of handle-ownership verification. Reopen only with a concrete defect or exploit crossing it. Veto window applies.
Generated by Claude Code
- added a commit that references this issue
on Aug 8, 2026
发现于 #5351 / #5696 的实施(PD #10 范围外发现,未在该批 PR 内修改;该批已把这条边界写进 ADR-0067/0119 修订与测试,作为决定而非缺口)。未认领。
事实
#5351 落地的同源校验按
TransactionScope(#5724)记录的属主 driver、以实例身份比对,决定一次写入是拒绝、carve-out 还是照常穿句柄。它能判的前提是:这个句柄的属主是谁,引擎知道。引擎只在自己
transaction()开事务时知道 —— 那一刻它把属主记进 txStore。两条路径上它不知道:ScopedContext的离散beginTransaction/commit/rollback三件套(packages/objectql/src/engine.ts)。它为沙箱 runner 而存在:hook/action 体的ctx.api.transaction(fn)被跨多个宿主事件循环轮次驱动,AsyncLocalStorage在setImmediate边界上不存活,所以句柄是显式穿的,根本不进 txStore;execCtx.transaction传进来的句柄。(⚠️ 常见的显式穿法是覆盖到的:
transaction()把trxCtx交给调用方、调用方再以{ context: trxCtx }传回,这个句柄按身份能匹配回 txStore 条目。这里说的是引擎从未开过的那些。)这两条上引擎手里只有一个不透明的 driver 对象,没有任何回指其 driver 的引用。所以没有诚实的比较可做,校验弃权、保持 #5351 之前的行为。
为什么当时不猜
猜有一个显而易见的候选 —— 「没有属主记录的句柄就当它属于默认驱动」。它两边都会错:
一个保证建立在猜测之上,比明确不覆盖更坏。
收口需要什么
让句柄属主可查,这是 driver 契约变更:
IDataDriver需要一条「这个句柄是不是我开的」的谓词(或让beginTransaction返回带属主标记的包装)。方向候选,交分诊定,本单不预设:IDataDriver.ownsTransaction?(handle): boolean—— 可选成员,没实现的 driver 维持今天的弃权语义;beginTransaction返回值统一包一层引擎侧的信封(属主 + 原句柄),由引擎在下发给 driver 前拆封。.transacting(trx)调用面,面更宽;可达性(诚实说明)
今天不是用户能撞到的缺陷:它是一条已声明的边界,而且只在「沙箱 hook 用离散三件套开事务」+「其中的写被路由到第二个数据源」同时成立时才有语义差别。按 #4949 平铺记录、给
finding标签,严重度交 PM 分诊定 —— 但它是 #5351 那条保证唯一没有覆盖到的角落,值得有个编号。Refs:#5351(裁 A 与同源校验)、#5696(契约收紧)、#5724(
TransactionScope属主记录)、ADR-0067 / ADR-0119 的 2026-08-06 修订(「The declared LIMIT of the same-origin gate」一节)、ADR-0034(ambient 事务)。钉住这条边界的测试:packages/objectql/src/engine-transaction-same-origin.test.ts的 “does NOT cover a foreign handle passed in from outside — declared, not overlooked”。