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!