CVE-2026-49882 AI-generated analysis
NFC tag reader/writer, Mifare Classic NDEF read (rw_mfc.cc): platform/system/nfc src/nfc/tags/rw_mfc.cc on Android 14/15, platform/packages/modules/Nfc libnfc-nci/src/nfc/tags/rw_mfc.cc on Android 16/17
In rw_mfc_handle_read_op (Mifare Classic reader in the NFC stack), the fix saves ndef_length and ndef_start_pos just before rw_nfc_decodeTlv on the first block read. It then treats a decode failure, or any growth of ndef_length, as a failure and restores both fields, so an inflated length no longer stays in the control block where it bounds the copy into p_ndef_buffer.
Root cause
In rw_mfc_handle_read_op, on the first block of an NDEF read (p_mfc->work_offset == 0), rw_nfc_decodeTlv(data) parses TLV data from the tag and updates p_mfc->ndef_length and p_mfc->ndef_start_pos in the control block. Before the fix, the code saved only the length (saved_length, re-read from p_mfc->ndef_length on every call). If the decoded length was larger, it logged SafetyNet id 178725766 and set failed = true. It did not restore p_mfc->ndef_length or p_mfc->ndef_start_pos, so the inflated value stayed in p_mfc. That value is the bound of the copy loop (p_mfc->p_ndef_buffer[p_mfc->work_offset] = p[offset] while work_offset < ndef_length) and of the completion check (work_offset >= ndef_length). Because saved_length was taken from the already-inflated field on each call, the comparison would no longer catch the inflation on any later call.
How the fix works
- src/nfc/tags/rw_mfc.cc:rw_mfc_handle_read_op: the function-scope uint16_t saved_length and its unconditional assignment are removed. Inside the work_offset == 0 branch, uint16_t saved_len = p_mfc->ndef_length and uint8_t saved_pos = p_mfc->ndef_start_pos are now captured immediately before rw_nfc_decodeTlv(data).
- The condition becomes `!rw_nfc_decodeTlv(data) || p_mfc->ndef_length > saved_len`, so a TLV decode failure or any increase in ndef_length counts as a failure. If the length grew, android_errorWriteLog(0x534e4554, "178725766") is emitted. Then p_mfc->ndef_length = saved_len and p_mfc->ndef_start_pos = saved_pos restore the control-block state, and failed = true is set.
- The copy guard is simplified from `!failed && saved_length >= p_mfc->ndef_length` to `!failed`, and the separate else branch (log + failed = true) is deleted. The copy loop body is unchanged (bounded by offset < len and work_offset < ndef_length). Within the shown region, ndef_length now cannot exceed its value before decoding.
- The commit message says control flow was also changed to stop further block reads on failure. The visible diff (1 file, +11/-11) has no separate hunk for this beyond the rollback and the failed flag.
Completeness
- Covers the site in rw_mfc_handle_read_op where rw_nfc_decodeTlv rewrites ndef_length/ndef_start_pos. Both fields are now restored on failure. Before the fix, the length was compared and nothing was restored.
- Decoded lengths smaller than or equal to the saved length are still accepted. Only growth is rejected.
- The fix relies on p_mfc->ndef_length at entry matching the size of p_mfc->p_ndef_buffer. The diff adds no explicit check of the copy destination against a buffer size, and the allocation and initialisation of p_ndef_buffer and ndef_length are not in the material.
- rw_nfc_decodeTlv itself is not modified. The fix is a rollback in the caller, not a change to the TLV parser.
- This commit does not touch other users of rw_nfc_decodeTlv or of ndef_length in rw_mfc.cc. The material does not show whether they have the same pattern.
Behavior changes
- If the first-block TLV decode yields a larger NDEF length, the read now fails and the original ndef_length and ndef_start_pos are restored. Before, the inflated values stayed in the control block.
- The SafetyNet log android_errorWriteLog(0x534e4554, "178725766") is now emitted only when inflation is detected. The old else branch also emitted it on a plain TLV decode failure.
- The 'FAILED finding TLV' debug message is now also logged for the inflation case.
- The per-block copy loop bounds are unchanged.
Per major
- Android 14: same fix: platform/system/nfc 7eaf8e16d9ad in android-security-14.0.0_r29, src/nfc/tags/rw_mfc.cc, log macro DLOG_IF(INFO, nfc_debug_enabled). Resolved; 7/8 lines of the patched region present at that tag.
- Android 15: same fix: platform/system/nfc 5dd24f8b4a6d in android-security-15.0.0_r17, identical hunk using LOG(VERBOSE). Resolved; 7/8 lines present.
- Android 16: same fix in a different repository: platform/packages/modules/Nfc b79e5fd8cf14 in android-security-16.0.0_r8, libnfc-nci/src/nfc/tags/rw_mfc.cc, identical hunk at line offsets 1197/1266. Resolved; 7/8 lines present.
- Android 17: same fix in a different repository: platform/packages/modules/Nfc 19fc329133fe in android-security-17.0.0_r1, libnfc-nci/src/nfc/tags/rw_mfc.cc, identical hunk at 1286/1355. Resolved; 7/8 lines present.
Unknowns
- The rest of rw_mfc_handle_read_op (how 'failed' is handled after the shown region) is not in the material. Neither is how p_ndef_buffer and ndef_length are allocated and initialised, so the buffer size the copy depends on is not visible.
- Whether later block reads still happened after the old failure path is not shown. The commit message says control flow changed to prevent further reads, but no separate hunk for this is visible.
- The body of rw_nfc_decodeTlv is not shown. It is not confirmed whether ndef_start_pos can change in a way that matters independently of the length; the fix rolls it back anyway.
- Which of the two bug ids (503557185, 503553866) corresponds to which report.