#1144143 clevis-initramfs: luks2_decrypt() can never report success (return inside a pipeline subshell)

Package:
clevis
Source:
clevis
Description:
automated encryption framework
Submitter:
Ignacio Martinez
Date:
2026-09-15 15:15:03 UTC
Severity:
normal
#1144143#5
Date:
2026-08-11 15:11:02 UTC
From:
To:
Dear Maintainer,

I ran into this while measuring boot times on a machine with two clevis
pins, and it
took me a while to work out why the console never told me clevis had done
anything.

luks2_decrypt() in /usr/share/initramfs-tools/scripts/local-top/clevis
loops over
LUKS2 tokens using a while-loop on the right-hand side of a pipeline. That
puts the
loop body in a subshell, so the "return 0" on the success path only exits
the
subshell, and control carries on to the trailing "return 1". The function
ends up
reporting failure every time, even when it has already decrypted the
passphrase and
written it to the askpass FIFO.

I checked this against Debian's own package rather than assuming it from
Ubuntu's
build: clevis-initramfs 20-1 from trixie, fetched with "apt-get download"
and
unpacked, has the code below.

Affected code:

    luks2_decrypt() {
        local CRYPTTAB_SOURCE=$1
        local PASSFIFO=$2
        cryptsetup luksDump "$CRYPTTAB_SOURCE" | sed -rn 's|^\s+([0-9]+):
clevis|\1|p' | while read -r id; do
            ...
            echo -n "${decrypted}" >"$PASSFIFO"
            return 0
        done

        return 1
    }

luks1_decrypt() has the same shape via "luksmeta show | while read".

The caller branches on that return value, which leaves the success path
unreachable:

    if luks2_decrypt "${CRYPTTAB_SOURCE}" "${PASSFIFO}"; then
        echo "Unlocked ${CRYPTTAB_SOURCE} with clevis"
    else
        OLD_CRYPTTAB_SOURCE=""
        sleep 5
    fi

Consequences:

 * "Unlocked <device> with clevis" never gets printed, so anyone watching
the console
   has no positive sign that clevis was the thing that worked.
 * Every attempt, successful ones included, clears OLD_CRYPTTAB_SOURCE and
sleeps 5
   seconds before going round again.
 * The retry loop has no way to tell success from failure, since it only
ever sees one
   of the two.

On a healthy boot none of this shows, because local-bottom/clevis kills the
loop as
soon as the volume opens. It surfaces on any host where a pin cannot be
satisfied.

If it helps, here is the shell behaviour on its own. I ran it under /bin/sh
(dash) and
again under bash, with the same result both times:

    shipped() {
        printf '2\n4\n' | while read -r id; do
            return 0
        done
        return 1
    }
    shipped; echo "exit=$?"      # prints exit=1

    fixed() {
        ids=$(printf '2\n4\n')
        for id in $ids; do
            return 0
        done
        return 1
    }
    fixed; echo "exit=$?"        # prints exit=0

Suggested fix — feed the loop from something other than a pipe, so the
function's own
return is reachable:

    luks2_decrypt() {
        local CRYPTTAB_SOURCE=$1
        local PASSFIFO=$2
        local ids
        ids=$(cryptsetup luksDump "$CRYPTTAB_SOURCE" | sed -rn
's|^\s+([0-9]+): clevis|\1|p')
        for id in $ids; do
            ...
            echo -n "${decrypted}" >"$PASSFIFO"
            return 0
        done
        return 1
    }

The same change would apply to luks1_decrypt().

Not applicable upstream, as far as I can tell — initramfs-tools does not
exist in
latchset/clevis, which ships dracut modules with a different
implementation, so this
looks like Debian-family packaging only.

I do not think this is a security issue. When a pin can be satisfied the
volume still
unlocks promptly and correctly; no key material is exposed, no unauthorised
unlock
becomes possible, and nothing cryptographic is weakened. It costs a missing
log line
and some needless retrying.

I have also reported it against Ubuntu, whose clevis 20-1ubuntu0.24.04.1
derives from
this package and carries the identical code, in case the two are useful to
read
together:
https://bugs.launchpad.net/ubuntu/+source/clevis/+bug/2163190

Thanks for your assistance with this!

#1144143#10
Date:
2026-09-15 15:11:49 UTC
From:
To:
Confirming on trixie (clevis-initramfs 20-1), with one correction to the impact
statement: the bug also costs ~5.5 s on a *healthy* boot when more than one
volume is unlocked by clevis. The fix is already in upstream v23 (Debian 23-1 in
sid/forky), so a trixie stable update looks worthwhile.

Setup: two LUKS2 volumes in crypttab (root + resume swap), both with a clevis
tpm2 pin (PCR 11), initramfs-tools, plymouth running.

Why the report's "healthy boot" statement does not hold with two volumes:
local-bottom/clevis only kills clevisloop once *all* local-top unlocking is
done. After the first volume succeeds, luks2_decrypt() returns 1, the loop
takes the failure branch (sleep 5, then the trailing sleep .5), and the askpass
for the second volume waits for it.

Measured with a temporary init-premount probe that logs `ps` to /dev/kmsg
every 200 ms (kernel timestamps):

2.71 s root: tpm2_createprimary / load / unseal
4.16 s root unlocked, cryptsetup open succeeds -> `sleep 5` appears
4.57 s askpass for the swap volume is already waiting
9.16 s sleep 5 ends -> sleep .5 -> sleep .1
9.99 s clevis starts on the swap volume
11.85 s swap unlocked -> `sleep 5` again (killed by local-bottom)

Workaround tested: a copy of the script in
/etc/initramfs-tools/scripts/local-top/clevis with `shopt -s lastpipe` added
(the script runs under bash, without job control), which makes the trailing
`while` run in the current shell.
Result: the window between "device-mapper: ioctl ... initialised" and
"PM: Image not found" (i.e. both clevis unlocks) went from 9.9 s to 5.0 s
(device-mapper at 4.14 s, resume check at 9.11 s), and the swap volume is now
unlocked right after the root volume.

Disclosure: I used an AI assistant (Claude) to help analyse the boot trace and
draft this message; I reviewed the analysis, applied the workaround and
measured the results on my own machine.

Upstream status: v20, v21 and v22 still have the pipeline form; v23 replaces
luks1_decrypt()/luks2_decrypt() with luks_decrypt(), which calls
clevis_luks_unlock_device() without a pipeline. Upstream issue
https://github.com/latchset/clevis/issues/342 (2021) describes the same"sleep 5 between each unlock" symptom and is still open.