Lenovo ThinkPad laptops with WWAN modules ship with a small proprietary binary that has to run before the modem will transmit. The binary exists for regulatory reasons: some modems are sold in an FCC-locked state, meaning the firmware enforces a transmit block until an unlock sequence is issued over the modem’s control interface. Lenovo’s solution is a compiled executable that handles this sequence, dropped into a path where ModemManager picks it up automatically.
A recent post on Hofstede’s blog documents replacing this blob with a 100-line Bash script. The result is functionally identical, fully auditable, and not tied to any specific architecture or libc version. The interesting part is not the script itself but what it reveals: the blob was doing almost nothing that couldn’t be expressed in plain shell.
What FCC Lock Actually Is
The FCC lock mechanism comes from a regulatory requirement for modems sold in the United States. The FCC’s rules require that RF-emitting devices operate within approved parameters. Some modem vendors implement this by shipping firmware in a locked state: the modem won’t transmit until it receives a vendor-specific unlock command, allowing the OEM to verify the device is installed in approved hardware before enabling RF output.
This is not a security feature in any meaningful sense. The unlock sequence is not secret in the way a cryptographic key is secret. It is closer to a handshake: the host sends a recognized command, the modem responds, and RF transmission is enabled. The protection is against unintended RF emission from an improperly integrated device, not against a determined attacker.
On Linux, ModemManager handles this via its fcc-unlock plugin system, introduced in version 1.14. When ModemManager detects a modem in a locked state, it looks for an executable in /usr/lib/ModemManager/fcc-unlock.d/ named by the modem’s USB vendor and product ID (e.g., 2cb7:0007 for a Fibocom L850-GL in MBIM mode). If found, it invokes that executable with the control port as an argument. The executable is expected to send whatever unlock sequence the modem requires and exit cleanly.
The Modem Control Interface
Modern WWAN modules typically expose multiple USB interfaces: one or more for data (MBIM, QMI, or NCM), and one or more serial ports for AT command access. The AT command set has been standardized since the early Hayes modem era, with 3GPP TS 27.007 defining the current baseline for cellular modems. Vendors add proprietary extensions on top of this.
For Fibocom modems, the FCC unlock typically involves an AT+GTFCCLOCK command. The general pattern across modems looks like this:
# Open the AT command port
exec 3<>/dev/ttyUSB2
# Send the unlock command
printf 'AT+GTFCCLOCK=0,1\r\n' >&3
# Read the response
read -t 2 response <&3
echo "Modem response: $response"
The exact command varies by modem model. Sierra Wireless modems on some ThinkPads use QMI protocol rather than raw AT commands, which requires qmicli instead of serial writes. But the principle is the same: a specific command goes in, an acknowledgment comes back, and the modem is operational.
Lenovo’s blob is doing this same sequence. The binary exists not because the task requires compiled code, but because shipping a shell script as a system component probably felt less appropriate to whoever packaged it, or because the original implementation used a library that made compilation natural. Either way, the functionality maps directly onto shell primitives.
Why Blobs Are a Problem Even When Small
A binary that does ten lines of work is still opaque. You cannot read it, cannot modify it for a slightly different modem variant, cannot verify it does only what it claims, and cannot run it on hardware the original developer didn’t target.
That last point matters more than it used to. ARM-based laptops are increasingly common, and ThinkPads are no exception: the ThinkPad X13s ships with a Snapdragon processor. A blob compiled for x86-64 will not run there. Lenovo may or may not ship a separate ARM binary; in practice, community users often find themselves without one. A shell script has no such constraint.
Glibc ABI stability helps x86-64 blobs survive across distributions and releases, but it is not guaranteed forever, and it does not help for musl-based systems like Alpine Linux. The script version works anywhere Bash works.
There is also an audit surface concern. ModemManager invokes these scripts as root. A blob that runs as root on modem initialization, on a machine with a network interface, deserves scrutiny. Reviewing a 100-line shell script takes five minutes. Reverse-engineering a compiled binary to equivalent confidence takes significantly longer and most users will skip it entirely.
What the Script Actually Does
The replacement script follows the same pattern ModemManager expects. It receives the USB VID:PID and the control port path as arguments, opens the AT command serial interface at the correct baud rate, sends the unlock sequence, verifies the response, and exits. Error handling covers cases where the port is busy, the modem is already unlocked, or the response is unexpected.
The key insight from the original post is that the author traced the blob’s behavior with strace and usbmon before writing the replacement. strace shows the system calls the binary makes: which device files it opens, which bytes it writes, which reads it waits on. usbmon, accessible via the kernel’s USB monitoring interface, captures the actual USB traffic. Together, these tools make a binary’s behavior legible without requiring disassembly.
# Capture USB traffic while the blob runs
sudo modprobe usbmon
sudo tcpdump -i usbmon1 -w modem_unlock.pcap
This is a well-worn technique in the Linux hardware community. The same approach has been used to reverse-engineer printers, scanners, drawing tablets, and dozens of other devices whose vendors shipped Windows-only binaries. The modem case is simpler than most because AT commands are human-readable text over a serial interface, not a proprietary binary protocol.
ModemManager’s Role and the Broader Pattern
ModemManager’s fcc-unlock.d design deserves credit for making this substitution easy. By defining a clear contract (executable named by VID:PID, receives control port as argument, exits 0 on success), it decouples the unlock mechanism from the rest of the modem management stack. You can drop in a shell script, a Python script, or a compiled binary, and ModemManager does not care.
This is the kind of seam that makes systems auditable. Compare it to a scenario where the unlock code is compiled into ModemManager itself or embedded in firmware: in those cases, substitution requires patching a major component or flashing hardware. The plugin architecture keeps the problem contained.
The ModemManager source tree includes several community-contributed fcc-unlock scripts for various modems. The Lenovo-specific blob could, in principle, be replaced by an upstreamed shell script that covers the same hardware. That would mean any user on any architecture gets working WWAN without depending on Lenovo’s binary distribution.
The Maintenance Argument
Lenovo will eventually stop shipping the blob. It will stop being compatible with some future kernel or userspace configuration, or Lenovo will simply stop including it in driver packages for older hardware. This is the standard lifecycle of proprietary firmware utilities.
A shell script in a public repository does not have this problem. It can be maintained by whoever uses the hardware, updated when modem firmware changes the expected command sequence, and adapted for new modem variants by editing a few lines. The knowledge encoded in the script is readable, not locked inside an ELF header.
This is not an argument that all binary blobs are unnecessary, many GPU firmware files, modem baseband images, and microcode updates genuinely require compiled binary delivery. But the category of “blob that is secretly just a sequence of ASCII commands to a serial port” is worth identifying and collapsing down to shell whenever possible. The 100-line bash script in this case is not a clever hack; it is the appropriate level of complexity for the task.