The USB stick that stole a ThinkPad's identity
A hardware audit tool reported a Lenovo laptop as a SanDisk flash drive. The cause was one convenient shell idiom, and it had silently overwritten the machine's identity three functions earlier.
The first real field run of my hardware audit tool produced a device record for a Lenovo ThinkPad T440. Almost everything on it was right: an i5-4200U, two cores and four threads, 8GB of DDR3 at 1600MHz, a 240GB drive, battery health at 68 percent, UEFI boot. Correct down to the detail.
Except the machine's name. The tool had recorded it as a LENOVO SanDisk 3.2Gen1, with a 128-character hexadecimal serial number.
That is not a laptop. That is the USB stick the tool had booted from.
What the tool does
The tool boots a device from a SystemRescue USB, reads its hardware directly from firmware and the kernel, and posts a complete profile to an inventory API. It exists because devices arriving for IT asset disposition are usually already wiped — there is no operating system to run an inventory agent on, so the specifications have to be read from outside.
Machine identity comes from DMI, early in the run:
MANUFACTURER=$(dmidecode -s system-manufacturer)
MODEL=$(dmidecode -s system-product-name)
SERIAL=$(dmidecode -s system-serial-number)Straightforward. And at that point in the run, correct — I confirmed it later by adding a debug print.
The convenient idiom
Storage comes later. lsblk has a pairs output mode that looks purpose-built
for shell consumption:
$ lsblk -P -o NAME,MODEL,SERIAL,SIZE,TRAN
NAME="sda" MODEL="SanDisk 3.2Gen1" SERIAL="0401d2ea..." SIZE="28.9G" TRAN="usb"Each line is already valid shell assignment syntax. The obvious thing to do — the thing I did — is to let the shell do the parsing:
lsblk -P -o NAME,MODEL,SERIAL,SIZE,TRAN | while read -r line; do
eval "$line"
# $NAME, $MODEL, $SERIAL are now set. Very tidy.
doneeval on that line defines shell variables named NAME, MODEL, SERIAL,
SIZE and TRAN.
Which are, of course, the same variables I had used for the machine's identity.
MODEL and SERIAL were quietly reassigned from the laptop's DMI values to
whichever drive the loop touched last. Nothing errored. Nothing warned. The
variables were still perfectly valid strings — they just now described a
different piece of hardware.
Why it looked plausible
This is what made it survive to the field instead of dying in testing. The
output was not garbage. It was a well-formed device record for a real device
that was genuinely plugged into the machine. MANUFACTURER was untouched, so
the record read "LENOVO SanDisk 3.2Gen1" — a Lenovo-branded something. At a
glance that is a slightly odd product name, not a category error.
A crash would have been better. A crash is a question. This was an answer, and it was wrong.
The fix
Three changes, each doing separate work.
Stop letting external output define variables. Parse into a namespace the rest of the script does not use:
while IFS= read -r line; do
D_NAME=$(printf '%s' "$line" | sed -n 's/.*NAME="\([^"]*\)".*/\1/p')
D_MODEL=$(printf '%s' "$line" | sed -n 's/.*MODEL="\([^"]*\)".*/\1/p')
# ...
done <<< "$(lsblk -P -o NAME,MODEL,SERIAL,SIZE,TRAN)"More verbose. It cannot reach outside its own prefix.
Audit internal drives only. The boot stick was never supposed to be in the inventory at all. Excluding removable media by transport, by the removable flag, and by sysfs means the USB the tool runs from is structurally incapable of appearing in a device record:
[ "$D_TRAN" = "usb" ] && continue
[ "$D_RM" = "1" ] && continue
[ "$(cat /sys/block/$D_NAME/removable 2>/dev/null)" = "1" ] && continueThis mattered far beyond the naming bug. The same tool can perform a secure erase. A drive-selection routine that could see the boot medium is a drive-selection routine that could one day erase it.
Read the model from the right DMI field. A separate wrinkle the first run
exposed: Lenovo puts the marketing name in system-version, not
system-product-name, which holds a type code like 20B6005TUK. Dell and HP
use system-product-name as you would expect. So the friendly name is
vendor-conditional.
What I took from it
eval is usually flagged for its injection risk, and that framing is why I did
not think twice here — lsblk output is not attacker-controlled in any
meaningful sense on a machine I have physically in front of me. The security
argument did not apply, so I skipped past it.
But the injection risk was never the only problem. eval hands an external
program the ability to write into my namespace. What broke was not security. It
was scope.
The wider lesson is about how the failure presented. Every serious bug I have found in this system has been silent: offline writes that vanished on sync, a dangling foreign key that wedged an upload queue, and now an identity quietly overwritten by a subroutine. None of them raised anything. They all produced confident, well-formed, wrong output.
Which is why the tool now prints its full captured profile before uploading, and why one junk asset named "LENOVO SanDisk 3.2Gen1" is still in my production database. I have not deleted it. It is a useful reminder that the plausible answer is the dangerous one.
Related reading
"It doesn't turn on" is the end of the chain, not the start
Diagnosing a dead laptop by working forward through the power sequence, instead of backward from the symptom. The board tells you where it stopped, if you ask it in the right order.
What a Certificate of Data Erasure should actually prove
Most erasure certificates document an intention. Making one document a verified event changes what you have to build — and means the generator has to be able to refuse.