Skip to content

Five bugs from the 2026-09-21 audit: diag execute bit, dead fstab branch, chroot on stdin, a lying summary line, a pin for every prompt - #1

Merged
doug445 merged 4 commits into
mainfrom
fix/audit-five-bugs
Sep 21, 2026
Merged

doug445 merged 4 commits into
mainfrom
fix/audit-five-bugs

Conversation

@doug445

@doug445 doug445 commented Sep 21, 2026

Copy link
Copy Markdown
Owner

What this fixes

Five bugs found in a full read of v1.6.0 by Claude Fable 5.1, each reproduced before it was fixed.

# Bug Where Fix
1 bin/linuxlocker-diag.sh was mode 100644; linuxlocker.sh diag exec's it and every document says sudo ./bin/linuxlocker-diag.sh. Both failed with permission denied. bin/linuxlocker-diag.sh mode 100755
2 The fstab device-path branch matched the partition name after the mountpoint. A real /dev/nvme0n1p3 / ext4 … line never matched, so it fell through to the "neither UUID nor PARTUUID" failure. The sed after it was already right. bin/luks-deploy.sh, stage 6b grep matches device, whitespace, /
3 The chroot phase was a 27 KB here-document on bash's stdin. Under the 64 KB pipe buffer bash 5.1+ reads it unbuffered through a pipe so children can share stdin; any child that reads stdin swallows the rest and the phase ends with a clean exit code, skipping UKI rebuild, signing and relabel. Reproduced: a script whose tail exits 5 returned 0 after one cat. bin/luks-deploy.sh, step 7 script written to /tmp/.luks-chroot.sh on the target, run with </dev/null, removed after and in the trap
4 The summary printed GRUB_ENABLE_CRYPTODISK=y, which stage 6c deliberately never writes. bin/luks-deploy.sh, summary states what was done
5 README: "every prompt has a LUKS_* variable behind it". 27 prompts, a handful pinnable; without a terminal a bare read died under set -e with no message. bin/luks-deploy.sh, README.md ask() helper: a pin answers and is logged; no pin and no terminal stops the run and names the variable. 14 new pins, documented in the script header and README

The inner-UUID-changed override keeps no pin on purpose: that state means the wrong device is open, and no fleet should answer it blind.

Verification

  • bash -n and shellcheck -S warning clean on every script
  • tests/cmdline-fixture-test.sh: 71 passed, 0 failed
  • tests/uki-fixture-test.sh: 41 passed, 0 failed, 1 skipped
  • tests/loopback-core-test.sh as root: 44 passed, 0 failed
  • ask() checked in isolation: a pinned value is taken and logged; with no pin and no terminal the run stops naming the variable
  • the new fstab grep matches /dev/X / … and ignores /dev/X /home …

Not in this PR

The audit also named three design gaps that are recorded in the README's Audit section and left open here: an end-to-end boot of the Fedora, Ubuntu and Arch GRUB paths, a console-keymap check for the initramfs, and sgdisk in the dependency map. No version bump; that is the maintainer's call.

🤖 Generated with Claude Code

doug445 and others added 4 commits September 21, 2026 03:22
…nch, chroot on stdin, a summary line that lied, and a pin for every prompt

1. bin/linuxlocker-diag.sh shipped as 100644. linuxlocker.sh exec's it and
   every document says `sudo ./bin/linuxlocker-diag.sh`; both failed with
   permission denied. The bug-report tool could not be run to report a bug.

2. The fstab device-path branch (stage 6b) looked for the partition name
   AFTER the mountpoint. On a real line the device comes first, so
   `/dev/nvme0n1p3 / ext4 ...` never matched and fell through to the
   "neither UUID nor PARTUUID found" failure. The sed on the next line was
   right all along; only the gate was wrong.

3. The chroot phase was a 27 KB here-document on bash's stdin. Under the
   64 KB pipe buffer, bash 5.1+ delivers it through a pipe and reads it
   unbuffered so children can share stdin — so any child that reads stdin
   (a hook that asks, a debconf prompt) swallows the rest of the script and
   the phase ends early with a clean exit code, skipping the UKI rebuild,
   signing and relabel. Reproduced: a script whose tail exits 5 returned 0
   after one `cat`. The phase is now written to /tmp/.luks-chroot.sh on the
   target and run with stdin from /dev/null.

4. The final summary printed "GRUB_ENABLE_CRYPTODISK=y", which stage 6c
   deliberately never writes. It now states what was actually done.

5. The README said every prompt has a LUKS_* variable behind it; 27
   prompts existed and only a handful were pinnable, and without a terminal
   a bare `read` died under set -e with no message. Every prompt now goes
   through ask(): a pinned value answers it and is logged; no pin and no
   terminal stops the run and names the variable. New pins: LUKS_CONFIRM
   (ENCRYPT|CONFIGURE|CONVERT, mismatch fatal), LUKS_STALE_MAPPER,
   LUKS_LIVE_OVERRIDE, LUKS_BATTERY_OVERRIDE, LUKS_UNMOUNT, LUKS_RESUME,
   LUKS_EXISTING, LUKS_DATA_PARTITION, LUKS_MISMATCH_OVERRIDE,
   LUKS_CROSS_DISK, LUKS_SUBVOL_MISMATCH, LUKS_FSCK, LUKS_FSCK_FORCE,
   LUKS_ALREADY_SHRUNK; LUKS_PROFILE (plus 'skip') also answers the
   re-costing menu after a LUKS1 conversion. The inner-UUID-changed
   override keeps no pin on purpose. README documents all of it, and the
   Audit section records this pass.

All three suites pass: cmdline 71/0, uki 41/0, loopback 44/0 (as root).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…in, and the README could be read otherwise

The 'skip' choice after a LUKS1 conversion was worded as "keep pbkdf2", and
the new LUKS_PROFILE=skip pin repeated it. pbkdf2 is a plain for loop over a
hash — no memory cost, every GPU core runs its own copy — and LinuxLocker
never writes it, never offers it, and does not treat a keyslot left on it as
hardened. The menu now says "defer the re-costing" with that explanation
under it, a deferred re-costing ends with a warning that the volume is not
hardened yet, the dry-run line for a GRUB without argon2 says the same, and
the README FAQ states it in bold with the for-loop reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…one — with cosmic time

A new subsection under the BitLocker/FileVault comparison: the loop written
out in four lines, the year it was standardized against the GPU of that
year, why N buys nothing against sixteen thousand cores, and what each
company put in front of its loop instead of replacing it. Ends with the
cosmic-era table from luks-tune.sh's own model, run over all three
functions with published RTX 4090 hashcat rates for the two loops.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…SHA-256 versus argon2id's bill for DRAM

BitLocker's chain and FileVault's PBKDF2 are built on SHA-256, the one hash
with a zettahash per second of purpose-built silicon already running. The
network's 1,000 EH/s and one Antminer S21 XP's 270 TH/s are worked through
the same cosmic-time model: six diceware words fall in 2.5 years and 72 days
respectively. argon2id at 4 GiB is bounded by DRAM bandwidth and capacity,
which no ASIC discounts; a generous thousand-part memory-bound farm still
leaves six words past the age of the universe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@doug445
doug445 merged commit 9d55671 into main Sep 21, 2026
14 of 15 checks passed
@doug445
doug445 deleted the fix/audit-five-bugs branch September 21, 2026 07:34
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.

1 participant