My Lenovo ThinkPad T14s Gen 4 (AMD Ryzen) shipped with an Intel XMM7560 LTE Advanced Pro modem soldered to the mainboard. Useful little thing: real LTE on the go, no tethering dance, no MiFi puck in the bag. The catch: out of the box, the modem refuses to register on the network. ModemManager dutifully detects it, the SIM is recognised, but on my machine AT+CFUN=1 would come back OK while the radio quietly stayed dark.
The reason is something called FCC lock, and the official fix from Lenovo is a package of proprietary helpers and shared libraries. I replaced Lenovo’s proprietary helper with a bash script that performs the same handshake in clear, auditable shell. Here is the why, the how, and the script.
Table of Contents
What FCC Lock Actually Is
If you have never run into this before, the term “FCC lock” sounds like DRM. It is not - or at least, not exactly. It is a regulatory compliance mechanism.
When a laptop ships with an embedded WWAN modem, the combination of the laptop chassis, the antenna layout, and the modem radio is what gets certified by the regulator. In the US that regulator is the FCC, and the relevant rule is the well-known 47 CFR Part 15 / Part 22-27 framework: a radio module is certified for a specific host device, with a specific antenna gain, in a specific physical configuration. If you yank the modem out of one laptop and stick it into another, the certification no longer applies.
To enforce this, modem vendors ship the radio in a state where the RF subsystem is administratively disabled until the host system performs a vendor-defined unlock handshake. The handshake is meant to be a challenge-response proof that the modem is sitting in a host the OEM has already certified together with the modem. Until it succeeds, attempts to fully enable the modem fail - upstream documentation talks about the enable step erroring out, and on my XMM7560 specifically the symptom was that AT+CFUN=1 came back OK while the radio never actually came up on the network.
In practice, it behaves like an OEM lock justified in regulatory terms. The same XMM7560 family has shown up across multiple OEM laptops, with different vendors using different unlock material. The radio is identical across hosts; what changes is the OEM-controlled signature that gates its use.
On Windows this is invisible because the Lenovo / Dell / HP driver stack performs the handshake at boot. On Linux the responsibility falls to ModemManager, which provides the unlock mechanism and even ships some upstream helper scripts itself - but, since version 1.18.4, leaves activation to either the user or a vendor package rather than running anything by default.
The ModemManager FCC Unlock Mechanism
ModemManager’s design here is sensible, and it is worth understanding properly because it is more nuanced than a single search path. There are three directories involved, each with a distinct role:
${libdir}/ModemManager/fcc-unlock.d/(e.g./usr/lib64/ModemManager/fcc-unlock.d/on Fedora) - this is where vendor-shipped, third-party unlock tools live. If Lenovo packages their helper for your distribution, this is where it goes./usr/share/ModemManager/fcc-unlock.available.d/- this is where ModemManager itself ships a small library of unlock scripts upstream. They are present on the system but not enabled./etc/ModemManager/fcc-unlock.d/- this is where you, the system administrator, opt in to one of the available scripts (or to your own) by symlinking it under the appropriate VID:PID name.
In all three locations the file is named after the modem’s USB or PCI VID:PID. For my XMM7560 the relevant name is 8086:7560 - Intel’s vendor ID, the modem’s product ID. ModemManager invokes the helper with the modem’s DBus path and the names of its control ports as arguments, and waits for it to exit successfully. If the helper returns 0, ModemManager continues bringing the modem up; if not, the modem stays disabled.
Two things are worth noting about this layout. First, the split between /etc and the upstream fcc-unlock.available.d directory exists precisely so that activation is an explicit decision, not something that happens by default - that change landed in ModemManager 1.18.4. Second, the helper can be any executable. A binary, a Python script, a shell script - ModemManager doesn’t care. That is the seam I used.
Lenovo’s Solution: A Proprietary Helper Package
Lenovo’s answer to this is lenovo-wwan-unlock, a GitHub-distributed package of proprietary helpers and shared libraries with setup scripts, SELinux policy and release packaging for several modules and systems. The README itself describes the project as “FCC and DPR unlock for Lenovo PCs”, which is worth pausing on: it is not just one unlock primitive. On supported systems Lenovo’s tooling also covers DPR (Dynamic Power Reduction) / SAR-related behaviour, the mechanism by which the laptop tells the modem to back off transmit power based on proximity sensors. So a vendor helper in this space can do more than flip a single lock bit.
You install the package, it drops the right files into the right places, and it works. It is also:
- Closed-source, with no way to audit what the helpers actually do to your modem
- Linked against specific runtime libraries, so on slightly newer or older distributions it can break in surprising ways
- The only path the vendor offers, on a free operating system, to use a piece of hardware you already paid for
The whole point of running Fedora on a ThinkPad is that I get to look at, modify, and trust every component of the user-space stack. Even if the proprietary helper is benign, accepting it as the permanent answer to “how do I use my modem” felt like the wrong default. So I went looking for an alternative.
The Alternative: A Shell Script That Does the Same Thing
The alternative turned out to be sitting in a ModemManager merge request, posted by Floris Stoica-Marcu. It is a single bash script, about 100 lines, CC0-licensed, that performs the entire XMM7560 unlock handshake using only bash, grep, awk, printf, xxd and sha256sum. No compiled code, no hidden state, no vendor SDK.
I want to be very clear about credit: I did not write this script. Floris did. What I did was take their script, drop it into /opt/unlocker/8086, and symlink it from ModemManager’s helper directory:
~ ❯ ls -lah /usr/lib64/ModemManager/fcc-unlock.d/
Permissions Size User Date Modified Name
drwxr-xr-x@ - root 11 Apr 12:14 .
drwxr-xr-x@ - root 11 Apr 12:14 ..
lrwxrwxrwx@ - root 11 Apr 12:14 8086:7560 -> /opt/unlocker/8086
That is the entire installation. Restart ModemManager.service, plug in the SIM, and watch the journal:
Apr 11 12:15:13 neochristop ModemManager[9628]: [modem0] state changed (enabling -> enabled)
Apr 11 12:15:15 neochristop ModemManager[9628]: [modem0] 3GPP registration state changed (registering -> home)
Apr 11 12:15:15 neochristop ModemManager[9628]: [modem0] state changed (enabled -> registered)
Apr 11 12:15:16 neochristop ModemManager[9628]: [modem0] state changed (connecting -> connected)
Apr 11 12:15:16 neochristop ModemManager[9628]: [modem0] simple connect state (10/10): all done
And the interface itself, with a real public IPv6 from my carrier:
~ ❯ ifconfig wwan0
wwan0: flags=209<UP,POINTOPOINT,RUNNING,NOARP> mtu 1500
inet 10.127.157.102 netmask 255.255.255.0 destination 10.127.157.102
inet6 fe80::b2ab:8cc1:6305:c055 prefixlen 64 scopeid 0x20<link>
inet6 2a02:3037:48d:1b55:f07e:922f:8154:e65 prefixlen 64 scopeid 0x0<global>
RX packets 199131 bytes 251330013 (239.6 MiB)
TX packets 52690 bytes 4544146 (4.3 MiB)
That is 240 MiB of traffic over LTE through an entirely auditable user-space path.
How the Unlock Actually Works
Here is the part that matters for anyone who wants to understand what they are running. The Intel XMM7560 unlock is a challenge/response handshake using a small set of vendor AT commands. Walking through the script in order:
1. Find the AT control port. ModemManager passes the modem’s control port names as arguments. The modem exposes both MBIM and AT ports; the AT port is what we need. The script picks it by reading /sys/class/wwan/<port>/type (Linux 5.14+) or by name pattern (older kernels):
for PORT in "$@"; do
grep -q AT "/sys/class/wwan/$PORT/type" 2>/dev/null && {
AT_PORT=$PORT
break
}
done
The chosen port is something like wwan0at0, which is then opened as /dev/wwan0at0.
2. Talk AT over a file descriptor. Bash can open a character device on a numbered file descriptor with exec, write a command, and read the reply. There is no socat, no chat, no helper - just bash redirection. The script wraps that in a small at_command function:
at_command() {
exec 99<>"$DEVICE"
echo -e "$1\r" >&99
read answer <&99
read answer <&99
echo "$answer"
exec 99>&-
}
The double read is there because the modem echoes the command on the first line and replies on the second. Crude, but it works on this modem.
3. Request a challenge. The XMM7560 implements a vendor command, AT+GTFCCLOCKGEN, that returns a fresh 32-bit pseudo-random nonce. This is the “challenge” half of the handshake:
RAW_CHALLENGE=$(at_command "at+gtfcclockgen")
CHALLENGE=$(echo "$RAW_CHALLENGE" | grep -o '0x[0-9a-fA-F]\+' | awk '{print $1}')
4. Compute the response. This is the only “magic” step, and it is not really magic at all. The response is a SHA-256 hash of the challenge concatenated with a fixed per-vendor secret, with some byte-order shuffling on each end. The Lenovo per-vendor secret in this script is the constant bb23be7f:
VENDOR_ID_HASH="bb23be7f"
HEX_CHALLENGE=$(printf "%08x" "$CHALLENGE")
REVERSE_HEX_CHALLENGE=$(reverseWithLittleEndian "${HEX_CHALLENGE}")
COMBINED_CHALLENGE="${REVERSE_HEX_CHALLENGE}${VENDOR_ID_HASH}"
RESPONSE_HASH=$(printf "%s" "$COMBINED_CHALLENGE" | xxd -r -p | sha256sum | cut -d ' ' -f 1)
TRUNCATED_RESPONSE=$(printf "%.8s" "${RESPONSE_HASH}")
REVERSED_RESPONSE=$(reverseWithLittleEndian "$TRUNCATED_RESPONSE")
RESPONSE=$(printf "%d" "0x${REVERSED_RESPONSE}")
In plain English:
- Take the challenge as a 32-bit value, write it as 8 hex chars in little-endian byte order.
- Append the Lenovo vendor secret (
bb23be7f). - Decode the resulting hex string into raw bytes (
xxd -r -p). - Compute SHA-256 over those bytes.
- Take the first 4 bytes of the digest, swap to little-endian, and convert to a decimal integer.
That decimal integer is the response.
This is the entire secret embedded in Lenovo’s helper: a four-byte constant and the byte-order conventions around it. There is no hardware-bound key, no TPM-sealed secret, no per-laptop derivation - just a baked-in constant that someone reverse-engineered out of the Lenovo helper years ago. The XMM7560 hardware does not actually verify that host certified it; it verifies that whoever is talking to it knows the constant.
That is why I am comfortable running this script. I am not defeating any hardware-bound secret here; I am reproducing the same host-side response the proprietary helper would generate, in shell instead of machine code.
5. Send the response and unlock the radio. Three more AT commands:
at_command "at+gtfcclockver=$RESPONSE" # verify the response
at_command "at+gtfcclockmodeunlock" # actually flip the lock bit
at_command "at+cfun=1" # bring the radio fully online
at_command "at+gtfcclockstate" # read the current lock state
If the final at+gtfcclockstate reports OK or 1, the modem is unlocked and the script exits 0. ModemManager picks up from there, runs its normal enable sequence, and the modem registers on the network. If the response is wrong, the modem responds with ERROR and the script retries up to nine times with a 0.5 second backoff.
That is the entire handshake. About 100 lines of bash, no compiled dependencies, every step inspectable.
Installing It Yourself
If you have the same modem - check with lspci | grep XMM for XMM7560 LTE Advanced Pro - the steps to swap the proprietary helper for this script are short. One caveat first about paths. On Fedora, Lenovo’s package and some third-party tooling may use /usr/lib64/ModemManager/fcc-unlock.d/, but the upstream user-facing path for manually enabled scripts is /etc/ModemManager/fcc-unlock.d/. The example below uses the upstream user path.
sudo install -d /opt/unlocker
sudo install -m 0755 8086 /opt/unlocker/8086
sudo ln -s /opt/unlocker/8086 /etc/ModemManager/fcc-unlock.d/8086:7560
sudo systemctl restart ModemManager.service
Then watch journalctl -u ModemManager -f while you bring the connection up in NetworkManager. You should see the modem walk through enabling -> enabled -> registered -> connecting -> connected and end up with an interface like wwan0 carrying real traffic.
If you want to see exactly what the script does on your machine, set FCC_UNLOCK_DEBUG_LOG=1 in the environment ModemManager runs in, and the script will append every step (including the AT exchanges) to /var/log/mm-xmm7560-fcc.log. That is a great way to convince yourself the script is doing nothing but the handshake described above.
If you have a different modem - a Sierra Wireless EM7565, a Quectel EM160, a different Intel chip - the same overall pattern (drop a helper into fcc-unlock.d named after the VID:PID) applies, but the AT command set and the secret are different. Don’t blindly use this script on hardware it wasn’t written for.
A Disclaimer About Jurisdiction
I have to write this part carefully, because the legal picture here is genuinely fuzzy, varies by country, and I am not a lawyer.
The FCC unlock mechanism exists because regulators in many jurisdictions require radio equipment to be certified as part of a specific host device. In the US that is the FCC’s modular and limited modular approval rules. In the EU the RED directive (2014/53/EU) imposes broadly similar obligations on the manufacturer. Other countries have their own equivalents. The OEM - in this case Lenovo - is the entity that holds the certification, and the unlock procedure exists so that the OEM can attest to the modem that “yes, I am still the host you were certified with.”
When you replace the OEM’s unlock helper with your own, you are bypassing that attestation. The modem does not become illegal hardware, and the radio characteristics do not change one bit - the script flips exactly the same lock bit the proprietary helper would have - but you may, depending on where you live and how strictly the rule is read, be operating a radio in a configuration the regulator did not formally bless. In some jurisdictions that is a non-issue for an end user modifying their own equipment for personal use. In others it might not be.
To be specific about the risks I am personally aware of:
- United States (FCC): US equipment authorization rules are not especially sympathetic to modified certified equipment, even though the direct compliance burden usually falls on manufacturers and grantees rather than on end users.
- European Union (RED): the user is generally not the addressee of the directive, but a modification that departs from the certified configuration creates an obvious compliance grey area around the original CE-marked configuration.
- United Kingdom, Switzerland, Canada, Australia: broadly similar to the EU model.
- Anywhere else: check your own regulator before you assume you are fine.
I am running this on my own laptop, on a SIM I personally own, on bands and with output power that the modem itself enforces in firmware. I consider the risk to me, in my jurisdiction, acceptable. I cannot make that call for you. If you are not comfortable with the regulatory ambiguity, keep using Lenovo’s helper package, or do not use the modem at all.
I will also point out the obvious: this changes nothing about the radio’s actual emissions. The modem firmware still respects the band tables, the antenna gain limits, and the transmit power caps that Intel certified. You cannot “unlock more bands” or “transmit at higher power” through this handshake. You are flipping the bit that says “I am allowed to talk to the network at all,” nothing else.
Wrapping Up
This was not a heroic hack. I did not reverse-engineer the modem, and I did not write the script. Floris Stoica-Marcu did the real work years ago in a ModemManager merge request. What I did was notice that Lenovo’s proprietary helper was not the only option, install the auditable alternative, and write down what it actually does. That still feels worth documenting, because free software does not stop mattering just because the proprietary piece is small.
References
- ModemManager merge request !1141 - the bash unlocker - the script and the discussion around it
- lenovo-wwan-unlock - Lenovo’s official proprietary helper package, for comparison
- ModemManager FCC Unlock Tools documentation - upstream description of the helper directory mechanism
- Directive 2014/53/EU (RED) - EU radio equipment directive
Comments
You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.
Search for the copied link on your Mastodon instance to reply.
Loading comments...