Skip to content

[finding] check:durability-log-level's read-seam wrapper recursion skips nested function bodies, so a driver read reached through a synchronous callback is invisible — an upper bound of 8 catches #12138

Description

@yinlianghui

Observation class — recording only, no verdict asserted, no pm:queue. Unassigned. Found while measuring for #11921 (PR #12137); ⛔ deliberately not fixed there — #11921 forbids its fix riding along with any other change.

Measured on origin/main @ 8619f9513.

What I found

isReadCall() (scripts/check-durability-degradation-log-level.mjs) follows a same-file wrapper by walking the wrapper's body with walkSameTickInclusive, which refuses to descend into anything runsLater() calls a later body — every FunctionExpression and ArrowFunction included.

For the try block that refusal is correct and documented at the top of the file:

Nested function bodies inside the try are NOT descended into: a callback registered inside a try (ctx.hook('kernel:ready', async () => …)) runs later and is not guarded by that catch.

For a wrapper body the question is a different one, and the same answer is not obviously right. A wrapper whose driver read sits inside a synchronously invoked callback —

async function loadAll(ids: string[]) {
    return Promise.all(ids.map((id) => this.driver.findOne(TABLE, { where: { id } })));
}

— does perform the read on the caller's behalf, inside the caller's try. runsLater() cannot tell that arrow apart from a deferred hook callback, so the wrapper reports "no read" and its caller's try/catch never enters the read-seam population at all.

How big, honestly bounded

Replacing walkSameTickInclusive with walkAll in the wrapper recursion only (a throwaway probe, not a proposed fix) moves the census over the three scan roots:

recognizer read seams
today (walkSameTickInclusive) 64
probe (walkAll) 72

⚠️ 8 is an upper bound, not the number. walkAll also descends into genuinely deferred callbacks — the ctx.hook(...) shape the header names — which correctly should not count. Telling "invoked now" from "invoked later" needs to know what the receiving method does with its argument, which is the same provenance problem #11921 is about. Somewhere between 0 and 8 of those 8 are real misses; nobody has read them one by one, and I did not, because that is the triage this filing is asking for rather than performing.

Direction of the error

Under-count, which is the safe direction for a gate: a real read seam is missing from the population rather than a fake one being added. But it is the same class of cost #11921 records — the read-seam census is the denominator for #5186, #6451, #9165, #8845 and #8901, and a denominator that silently omits members is as hard to reason about as one that silently includes them.

Nothing is broken and no verdict is wrong today: this is an invisibility, not a misclassification, so no site's grade changes either way.

Related

Dedupe performed

⚠️ The prescribed channel was unavailable, and this is declared rather than papered over: direct GitHub REST from this seat returns "GitHub access is not enabled for this session" to curl and gh is not installed — the condition already filed as #12123. The dedupe therefore ran through one bounded MCP list_issues call: 24 open finding issues, a non-empty control. None addresses isReadCall, the wrapper recursion, walkSameTickInclusive, or the read-seam population; the nearest neighbours are unrelated gates (#12064 bare-root-worklist, #12046 turbo task-graph). A local grep over the gate file confirms the narrowness is recorded for the try side only (top-of-file, "Nested function bodies inside the try") and nowhere for the wrapper side.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions