Skip to content

[finding] A settings READ in the pre-bind window silently resolves to manifest defaults instead of the persisted sys_setting row #10250

Description

@os-warren

Found while implementing #10159 (the write half of the same window). Filed separately rather than widening that card, and deliberately not fixed there: closing it would change what a real population observes, which #10159's fix was explicitly scoped not to do.

Observation

SettingsService.loadRows branches on if (this.engine) exactly as upsertRow does:

privateasyncloadRows(namespace: string,userId: string|null): Promise<SettingsRow[]>{if(this.engine){/* … engine.find(this.objectName, …) … */}returnthis.memory.filter(/* … */);}

The engine is bound in one place — SettingsServicePlugin registers a kernel:ready hook from its start() and calls bindEngine inside it. Hooks fire in registration order and every plugin's init() runs before any plugin's start(), so every kernel:ready hook registered from an init() runs before that bind (the mechanism is written up in full on #10159).

So a get() / getNamespace() issued in that window reads the in-memory fallback, which at boot holds nothing — and the caller receives the manifest's declared default, with locked: false and no diagnostic, while a real persisted row with a different value sits in sys_setting unread.

Measured

The window itself is measured on #10159's branch (claude/issue-10159-settings-write-before-bind), where the in-window probe records:

serviceResolvableAtReady : true
engineBoundAtReady : false
readAtReady : resolved:"never" <- the manifest default

"never" is the specifier's declared default; the read is answered without the engine ever being consulted.

What is NOT measured — and why this is graded finding

No shipped in-window reader has been identified. The repo has ten-odd settings.get(...) / getNamespace(...) call sites (resolve-authz-context.ts, auth-plugin.ts, lifecycle-service.ts, email-plugin.ts, sms-plugin.ts, storage-service-plugin.ts, …), and whether any of them is reached from a kernel:ready hook registered during init() has not been established. That is the measurement this card needs before it is worth acting on: without it there is no evidence anyone is harmed, and the fix would be speculative.

Why it is worth recording anyway

The write half was recorded on the same reasoning and turned out to be real. And the read half is the quieter of the two: a wrong-but-plausible value (the declared default) is harder to notice than a missing one, and the locked: false that accompanies it is an assertion about persisted state made without reading persisted state.

Note on scope, if this is ever picked up

The refusal shape used for the write half is not transferable here. #10159's fix deliberately leaves reads open, because refusing an ordinary boot-time read would turn a correct startup sequence into an error — an in-window read of a defaulted setting is a legitimate, common thing to do. Any fix here has to answer "what should a boot-time reader see instead?", which is a different question from the write side's "when is the earliest safe moment to write?".

Adjacent, not a duplicate

#10221 also concerns a boot-time settings read, but on the far side of this window: there the read reaches the database and errors with sys_settingno such table (so the engine was already bound). This one never reaches the database at all, and produces no log line of any kind.

Filed unassigned.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions