fix(security): delete the plaintext LFS mount rather than gate it - #919
Merged
Conversation
The Unprovisioned mount arm mounted LFS at the userdata partition head with no EncryptedBlockDevice, and formatted it on InvalidSuperblock. #866 asked for it to be removed from production or build-gated as non-production. Removing it outright, because gating it would have produced a branch no build can execute. It was reachable ONLY in production. mount_plan cannot reach the arm without secure_boot_ok, and kinit sets that at exactly one place, only when BOOT_KEY_IS_PRODUCTION -- a dev-anchor build boots degraded with it false, by design, so the deliberately-public committed dev key cannot establish trust on a device. Host tests always build with the dev anchor and QEMU never verifies at all. So the "legacy dev/transition path" its own comment described was wrong about its own reachability: no dev build, no host test and no QEMU witness could take it, and the only configuration that could was the one where a plaintext persistent root is forbidden. That inverts the usual disposition. A compatibility path that exists in no configuration but the one it must not run in has nothing left to be compatible with. What changes on a device: an unprovisioned device whose first-boot setup did not produce a verified passphrase -- skipped, interrupted, or unable to reach the operator -- now falls back to the initramfs instead of creating a plaintext root. It has never held a secret, so it has no userdata to lose by declining to mount. MountPlan loses its third variant, so the property is structural rather than asserted: there is no plaintext answer to return. The new exhaustive test walks the whole input space rather than the states someone remembered to enumerate, and requires every Encrypted result to carry a derived key on a verified boot. This also retires the plain arm's half of #360. The encrypted mount separates a never-formatted payload from a damaged one with provisioned_this_boot; the plain arm had no such marker and formatted anyway, and the comment explaining why that was tolerable is gone with the code it excused. Closes #866
forkwright
force-pushed
the
fix/866-plain-mount
branch
from
August 21, 2026 10:49
315d8d6 to
705608c
Compare
forkwright
pushed a commit
that referenced
this pull request
Aug 21, 2026
🤖 I have created a release *beep* *boop* --- ## [0.9.0](v0.8.7...v0.9.0) (2026-08-21) ### Features * **security:** derive secret verifiers with Argon2id from page-backed memory ([#915](#915)) ([4667bb8](4667bb8)), closes [#272](#272) * **security:** enforce the ~77-bit boot-secret floor at first-boot setup ([#912](#912)) ([f071f42](f071f42)) * **watchdog:** pet only while every liveness owner is still advancing ([#920](#920)) ([33791a8](33791a8)), closes [#875](#875) ### Bug Fixes * **ci:** count the ledger's runnable tests after combining profiles, not before ([#916](#916)) ([653e0df](653e0df)), closes [#855](#855) * **clock:** separate authentication from precedence and refuse implausible time ([#906](#906)) ([b7f9205](b7f9205)) * **crypto:** enable zeroize on every keyed cipher crate and prove it at compile time ([#900](#900)) ([e2218fc](e2218fc)) * **csprng:** credit cadence departure, not raw sample-to-sample bit flips ([#901](#901)) ([7b01d75](7b01d75)) * **kernel:** map the whole reserved window, and derive its bound from one place ([#918](#918)) ([25ccf87](25ccf87)), closes [#917](#917) * **klesis-core:** reject an out-of-domain CSQ reading and cover the six untested guards ([#909](#909)) ([1f174d6](1f174d6)) * **krypta:** hold ratchet key material in a type whose drop scrubs it ([#904](#904)) ([0c7f6df](0c7f6df)) * **krypta:** make the ratchet commit atomic and stop it being duplicable ([#908](#908)) ([dd3ba99](dd3ba99)) * **matrix_crypto:** drop the Olm ratchet fields nothing can advance, scrub what the handshake used ([#903](#903)) ([d733857](d733857)) * **matrix_crypto:** name the Olm pre-key derivation for what it is ([#913](#913)) ([8c217c2](8c217c2)) * **pteron:** close the four remaining SMP findings ([#910](#910)) ([e96b7c7](e96b7c7)) * **security_mode:** store a versioned Sentinel-exit PIN verifier with a per-device salt ([#896](#896)) ([bbf9c86](bbf9c86)) * **security:** delete the plaintext LFS mount rather than gate it ([#919](#919)) ([cff4e35](cff4e35)), closes [#866](#866) * **security:** gate radio-affecting threat action on calibration and authorization ([#921](#921)) ([c9562fd](c9562fd)) * **security:** require a real provisioning anchor, or carry none at all ([#922](#922)) ([a5016e8](a5016e8)) * **security:** verify every stored secret through one salted, iterated record ([#898](#898)) ([b5cb2c2](b5cb2c2)) * **sms:** apply the SCTS timezone octet so decoded timestamps are UTC ([#905](#905)) ([ad048fe](ad048fe)) * **stegnos:** reject a persisted PBKDF2 iteration count above the maximum ([#899](#899)) ([87fdfad](87fdfad)) * **thumos:** close all eight findings in the low-severity kernel batch ([#911](#911)) ([8160589](8160589)) * **thumos:** close the three live findings in the wave-1 batch ([#907](#907)) ([891da7e](891da7e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The finding inverted its own description
MountPlan::Plainmounted LFS at the userdata partition head with noEncryptedBlockDevice, and formatted it onInvalidSuperblock. Its comment called it a "legacy dev/transition path", and #866 asked for it to be removed from production or build-gated as non-production.It was reachable only in production.
mount_plancannot reach that arm withoutsecure_boot_ok, andkinitsets that at exactly one place — gated onBOOT_KEY_IS_PRODUCTION:A dev-anchor build boots degraded with it false, deliberately, so the committed public dev key cannot establish trust on a device. Host tests always build with the dev anchor. QEMU never verifies at all.
So no dev build, no host test and no QEMU witness could ever take that arm, and the only configuration that could was the one where a plaintext persistent root is forbidden. A compatibility path that exists in no configuration but the one it must not run in has nothing left to be compatible with — which is why this deletes it rather than taking the gating option the issue offered. Gating would have produced a branch no build can execute and no test can reach.
What changes on a device
An unprovisioned device whose first-boot setup did not produce a verified passphrase — skipped, interrupted, or unable to reach the operator — now falls back to the initramfs instead of creating a plaintext root. It has never held a secret, so it has no userdata to lose by declining to mount.
The property is structural now, not asserted
MountPlanloses its third variant, so there is no plaintext answer to return. The new exhaustive test walks the whole input space — every combination ofemmc_ok,secure_boot_ok, all fivePreambleLoadstates andpassphrase_ok— rather than the states someone remembered to enumerate, and requires everyEncryptedresult to carry a derived key on a verified boot over a readable preamble.One thing clippy found that was worth more than silencing
With the arm gone,
preamblebecame an unused parameter — the decision no longer consulted it._preamblewould have compiled.Instead the check is now stated:
Encryptedrequirespassphrase_okand a preamble ofProvisionedorUnprovisioned. Today that is implied —boot_passphrase_plananswersSkipfor the other three, so no key can exist for them — but the implication lives in a different function. The cost of it ever ceasing to hold is mounting the payload of a device whose preamble could not be read, which is exactly what #621 established must never happen. Three new assertions pin that unknown stays locked even if a key somehow exists.Also retires the plain arm's half of #360
The encrypted mount distinguishes a never-formatted payload from a damaged one via
provisioned_this_boot. The plain arm had no such marker and formatted anyway; the comment explaining why that was tolerable is gone with the code it excused.Verified
Nine-configuration
scripts/kernel-clippy.shclean,cargo fmt --checkclean. Full kernel suite on the host target: 2463 tests, 43 failures — exactly the unmodified-mainbaseline, compared name-by-name against a captured baseline set rather than by module prefix. 129 lines removed fromkinit.rs;docs/capability-inventory.tomlupdated so it no longer describes a plaintext path that does not exist.Closes #866