Passport to Purgatory: Infinite Loops in gmrtd
Jan 27, 2026·6 min read·4 visits
Executive Summary (TL;DR)
The `gmrtd` library, used to read ePassports, blindly trusts the length declared by the NFC chip. By presenting a chip that claims to hold 4GB of data, an attacker can force the reader into an infinite read loop, causing CPU exhaustion and an eventual Out-of-Memory (OOM) crash.
A Denial of Service (DoS) vulnerability in the gmrtd Go library allows a malicious NFC chip to crash reading applications by advertising a fake, massive file size (4GB+), leading to memory exhaustion.
The Hook: When the Passport Attacks You
We usually think of NFC security in one direction: a malicious reader skimming your credit card or passport in a crowded subway. We worry about shielding sleeves and cryptograms. But rarely do we consider the inverse scenario: what if the passport itself is the weapon?
Enter gmrtd, a Go library designed to handle the heavy lifting of parsing Machine Readable Travel Documents (MRTDs) via the ISO7816 protocol. It implements the complex dance of APDU commands required to authenticate and read data groups (DGs) from the chip—like your face photo or biometric fingerprints.
In a perfect world, hardware adheres to spec. In the real world, hardware lies. CVE-2026-24738 is a classic case of "implicit trust." The library assumes that if an NFC chip says, "I have a file here, and it is 4 gigabytes long," the chip is telling the truth. This assumption turns every border control kiosk or ID-verification mobile app using this library into a sitting duck for a simple resource exhaustion attack.
The Flaw: Trusting the Length Byte
The vulnerability lies in how gmrtd handles BER-TLV (Basic Encoding Rules - Tag-Length-Value) encoded data. When you read a file from an ePassport, the first thing you get is a header. This header tells you what the data is (Tag) and how big it is (Length).
In the smart card world, lengths can be encoded in "short form" (one byte) or "long form." Long form allows you to specify massive sizes by using a prefix like 0x84, followed by four bytes representing the size. This theoretically allows a file size up to 4GB (0xFFFFFFFF).
The flaw is embarrassingly simple: gmrtd parsed this length and immediately committed to reading that many bytes. It didn't ask, "Does a 4GB photo make sense on a low-power smart card?" It didn't ask, "Do I even have enough RAM to hold this?" It just shrugged and entered a loop.
This is a logic bug class known as Uncontrolled Resource Consumption (CWE-400). By failing to validate the upper bound of the length field against a sanity limit (like the max size of an ISO7816 file or available memory), the code voluntarily enters a death spiral.
The Code: The Smoking Gun
Let's look at the crime scene in iso7816/nfc_session.go. The ReadFile function is responsible for fetching the Data Group. Here is the logic before the fix:
func (nfc *NfcSession) ReadFile(fileId uint16) (fileData []byte, err error) {
// [1] Read the header from the chip
fileHeader, err := nfc.ReadBinary(0, 4)
// [2] Parse the Tag and Length.
// tmpTlvLength comes directly from the malicious chip.
tmpTag, tmpTlvLength, tmpBuf, err := tlv.ParseTagAndLength(fileHeader)
// [3] Set the target. No validation occurs here.
totalBytes = int(tmpTlvLength)
totalBytes += 4 - tmpBuf.Len()
// [4] The Loop of Doom
if fileBuf.Len() < totalBytes {
for {
// Calculate next chunk size
bytesToRead := min(maxReadAmount, totalBytes-fileBuf.Len())
// Read the chunk
tmpData, err := nfc.ReadBinaryFromOffset(fileBuf.Len(), bytesToRead)
// Append forever until OOM or totalBytes is reached
fileBuf.Write(tmpData)
}
}
}The code at [3] is the fatal error. It takes tmpTlvLength (which could be 4,294,967,295) and sets it as the goalpost. The for loop at [4] will then execute millions of times, issuing READ BINARY commands and appending the results to a byte slice until the Go runtime panics with an Out of Memory error or the CPU melts.
The Exploit: The Infinite Passport
To exploit this, you don't need a supercomputer. You need a $150 Flipper Zero or a rooted Android phone running a card emulation app. The goal is to emulate an ePassport that looks normal during the initial handshake but becomes a monster when the reader asks for data.
Here is the attack chain:
- Handshake: The attacker presents the emulated device. The reader (victim) performs the standard ISO7816 selection and authentication.
- The Trap: The reader requests
DG2(the facial image). This is standard procedure for almost all ID verification flows. - The Trigger: The emulated chip responds to the first
READ BINARYcommand with a valid Tag (0x61) but a weaponized Length:0x84 0xFF 0xFF 0xFF 0xFF(4GB). - The Stall: The reader sees the length and starts requesting chunks of 256 bytes. The attacker's emulator simply replies with 256 bytes of garbage (zeros) and a success status word (
0x90 0x00) for every request.
The victim application hangs immediately. If the application is single-threaded or the reading happens on the main UI thread, the interface freezes. Eventually, the operating system kills the process for consuming too much memory.
The Fix: Trust Nobody
The remediation, applied in v0.17.2, is a lesson in defensive programming. The maintainers implemented hard limits on what the library is willing to accept from the external world. They realized that a passport photo is never going to be 4GB. In fact, it's rarely going to be larger than 30-40KB.
The patch introduces two key constraints in iso7816/nfc_session.go:
- Max TLV Length Cap: A constant
READ_FILE_MAX_TLV_LENGTHset to 65,535 bytes (64KB). If the chip declares a size larger than this, the library immediately aborts with an error. - Max Chunk Count: A constant
READ_FILE_MAX_CHUNKSset to 1000. This prevents a "slow loris" style attack where the chip returns valid total lengths but sends data 1 byte at a time to waste CPU cycles.
Here is the diff for the fix:
// The Fix: Sanity Checks
if tmpTlvLength > nfc.readFileMaxTlvLength {
return nil, fmt.Errorf("[ReadFile] TLV length exceeds permitted maximum")
}
// Inside the loop
if chunkCnt >= nfc.readFileMaxChunks {
return nil, fmt.Errorf("[ReadFile] Max chunks reached")
}This turns an OOM crash into a graceful error message: "[ReadFile] TLV length exceeds permitted maximum". The application survives, logs the error, and the attacker is left standing there looking silly.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
gmrtd gmrtd | < 0.17.2 | 0.17.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 (Uncontrolled Resource Consumption) |
| Attack Vector | Physical (NFC) |
| CVSS | 4.6 (Medium) |
| Impact | Denial of Service (DoS) |
| Patch | v0.17.2 |
| Exploit Status | PoC Available |
MITRE ATT&CK Mapping
The software does not properly restrict the size or amount of resources that are requested or consumed, which can be used to consume all available resources.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.