Skip to content

fix(dm-verity): stop substituting a flat file digest for a Merkle root - #105

Merged
srpatcha merged 1 commit into
embeddedos-org:masterfrom
Kartikey1306:fix/dmverity-no-fake-root-hash
Sep 1, 2026
Merged

srpatcha merged 1 commit into
embeddedos-org:masterfrom
Kartikey1306:fix/dmverity-no-fake-root-hash

Conversation

@Kartikey1306

@Kartikey1306 Kartikey1306 commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

eos_dmverity_create() has a fallback. If veritysetup is missing or fails, it runs sha256sum <image> and stores the result in dv->root_hash:

if (!dv->root_hash[0]) {
    /* Fallback: compute SHA-256 of the image as a placeholder root hash */
    snprintf(cmd, sizeof(cmd), "sha256sum \"%s\" | cut -d' ' -f1", image_path);
    ...
}

A dm-verity root hash is the root of a Merkle tree over the image's blocks. That structure is the whole point — it is what lets the kernel check each block as it is read, lazily, at read time. A flat digest of the whole file is a different thing that happens to be the same length and the same shape, so the substitution is invisible in every printout and every string comparison.

What it is not is usable:

  • eos_dmverity_generate_table() emits that value into a dm-verity table the kernel will reject
  • anything treating root_hash as an integrity token holds a hash with none of dm-verity's properties — no per-block verification, no detection of tampering after mount

A hash device that could not be built is a failure. Reporting one is more useful than inventing a hash that cannot verify anything.

Second, in the same function

dv->verified = (dv->root_hash[0] != '\0');
return dv->verified ? 0 : -1;

verified means an image was checked against a known-good root hash. Creating a hash device is not that check — it establishes the root hash, it does not verify anything against it. Setting the flag here made every successful create() indistinguishable from a completed verification to any caller reading the field, eos_dmverity_dump() included.

eos_dmverity_verify() already sets it correctly, from veritysetup verify's exit status, and is now the only place that does. create() returns the same 0/-1 as before, so its own callers are unaffected. eos_dmverity_init() memsets the struct, so verified starts false and stays false until something actually verifies.

Verification

eos_linux_security builds clean
full Release suite 25/28

The three Not Run are services/compat/src/os_adapter_posix.c needing pthread_mutex_timedlock() / sem_timedwait(), which macOS does not provide — #96's subject, unrelated to this change. Linux CI builds all 28.

Deliberately not in this PR

This file drives veritysetup, cryptsetup and friends through system() / popen() with paths interpolated straight into shell strings — ten call sites. That is a real injection surface and its own piece of work; folding it into a correctness fix would bury both. Happy to take it as a follow-up.

@Kartikey1306

Copy link
Copy Markdown
Contributor Author

Now stacked on #104 (which is on #102). Merge order: #102 → #104 → this.

The two red checks were master's, not this PR's — it touches one file, services/linux/src/linux_security.c, and neither failure comes near it:

check cause fixed by
Cross-compile ARM64 kernel EBLDR_VERIFY_STAGE1 is ON but board qemu_arm64 has no stage-1 linker script — the eos simulation workflow configures eBoot without the flag the error names #102
Build & Test (Linux x86_64) hangs in Run C unit tests — test_net wraps its connect/send in assert(), which Release deletes, so the echo thread blocks in accept() forever #104 (root cause) + #102 (ctest --timeout so it reports instead of hanging)

Rebasing onto the chain rather than leaving it red, since neither is something this change can fix from where it sits. Once #104 merges this collapses back to the single file it actually changes.

eos_dmverity_create() has a fallback: if veritysetup is missing or fails, it
runs `sha256sum <image>` and stores the result in dv->root_hash.

A dm-verity root hash is the root of a Merkle tree over the image's blocks --
that structure is what lets the kernel check each block as it is read, at read
time. A flat digest of the whole file is a different thing that happens to be
the same length and shape, so the substitution is invisible in every printout
and comparison. What it is not is usable: eos_dmverity_generate_table() emits
that value into a dm-verity table the kernel will reject, and anything treating
root_hash as an integrity token holds a hash with none of dm-verity's
properties.

A hash device that could not be built is a failure. Reporting one is more
useful than inventing a hash that cannot verify anything. Fallback removed.

Second, in the same function:

    dv->verified = (dv->root_hash[0] != '\0');
    return dv->verified ? 0 : -1;

`verified` means an image was checked against a known-good root hash. Creating
a hash device is not that check -- it establishes the root hash, it does not
verify anything against it. Setting the flag here made every successful
create() indistinguishable from a completed verification to any caller reading
the field, including eos_dmverity_dump(). eos_dmverity_verify() already sets it
correctly from `veritysetup verify`'s exit status, and is now the only place
that sets it. create() returns the same 0/-1 as before, so its own callers are
unaffected.

eos_dmverity_init() memsets the struct, so verified starts false and stays
false until something actually verifies.

Verified: eos_linux_security builds clean; full Release suite 25/28 with the
three Not-Run being services/compat's pthread_mutex_timedlock/sem_timedwait gap
on macOS (embeddedos-org#96's subject, unrelated). Linux CI builds all 28.

Not addressed here, deliberately: this file drives veritysetup, cryptsetup and
friends through system()/popen() with paths interpolated into shell strings --
ten call sites. That is a real injection surface and a separate piece of work;
folding it into a correctness fix would bury both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Kartikey1306
Kartikey1306 force-pushed the fix/dmverity-no-fake-root-hash branch from 218b9c9 to cf4f849 Compare September 1, 2026 09:41
@srpatcha
srpatcha merged commit eddb6ac into embeddedos-org:master Sep 1, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants