CVE-2026-0084 AI-generated analysis
platform/packages/modules/Nfc, NfcNci card emulation (HostEmulationManager)
Makes the NFC HostEmulationManager unbind non-preferred HCE service connections right away in two cases, instead of keeping the binding alive until the next NFC transaction: when the preferred service is unset (the app leaves the foreground), and when the regular connection's binding dies.
Root cause
In multiple-binding mode (isMultipleBindingSupported(), introduced with the change referenced as b/365533082; the TODO links this to Flags#allowMultipleHceBindings), the no-arg unbindServiceIfNeededLocked() keeps bindings in mComponentNameToConnectionsMap. It has a branch whose condition starts with mServiceName == null or the DONT_IMMEDIATELY_UNBIND_SERVICES compat change. Per the new javadoc, those bindings are cleared only when mUnbindInactiveServicesRunnable runs after a transaction completes. Two paths called this deferred variant: the preferred-service update path (the service == null branch, i.e. preferred service unset) and the onBindingDied callback of a non-payment ServiceConnection. As a result, per the commit message, when an app with a registered HCE service called setPreferredService and then moved to the background or was closed, the NFC stack kept the binding to the app's service alive until the next NFC transaction or reboot.
How the fix works
- Commit a90c62ca6e75 (android-security-16.0.0_r7), NfcNci/src/com/android/nfc/cardemulation/HostEmulationManager.java: moves unbindInactiveServicesLocked() out of the anonymous mUnbindInactiveServicesRunnable into a private method on the class. The logic is unchanged: for every entry in mComponentNameToConnectionsMap whose key differs from mAidCache.getPreferredService() and whose mMessenger != null, it calls mContext.unbindService(), and it keeps only the preferred entry. The commit also adds a Log.d line.
- Same commit: adds an overload, unbindServiceIfNeededLocked(boolean force). The no-arg version now delegates with force=false, which keeps the documented deferred behavior for polling loops and for switching services within a transaction. With force=true and isMultipleBindingSupported(), the method calls unbindInactiveServicesLocked() right away and returns before reaching the deferral branch.
- Same commit: on the preferred-service update path (hunk starting at old line 766: if service != null bindServiceIfNeededLocked(userId, service), else unbind), the else branch now calls unbindServiceIfNeededLocked(/* force */ true).
- Commit 6bc72fe8b34e (android-security-16.0.0_r8), same file: in the ServiceConnection.onBindingDied(ComponentName) at about old line 1770 (not the payment connection's onBindingDied), unbindServiceIfNeededLocked() becomes unbindServiceIfNeededLocked(/* force */ true).
Completeness
- The force path only takes effect when isMultipleBindingSupported() is true. Otherwise the code falls through to the existing unbind logic, which the diff does not change.
- unbindInactiveServicesLocked() keeps the entry equal to mAidCache.getPreferredService(). What that call returns when the preferred service is unset is not shown.
- In unbindInactiveServicesLocked(), non-preferred connections with mMessenger == null are not unbound through mContext.unbindService(), but they are dropped from mComponentNameToConnectionsMap. This logic existed before the fix and is carried over unchanged.
- The payment ServiceConnection's onBindingDied (context at line 1624, which rebinds through bindPaymentServiceLocked) is not touched by either commit.
- Other callers of the no-arg unbindServiceIfNeededLocked() keep the deferred behavior on purpose, as the new javadoc documents.
Behavior changes
- When the preferred HCE service is unset (multiple-binding mode), non-preferred HCE service bindings are unbound immediately instead of when mUnbindInactiveServicesRunnable runs after the next transaction.
- When the regular service connection's binding dies, inactive bindings are cleared immediately.
- A new Log.d line is written for each unbound service in unbindInactiveServicesLocked(). An IllegalArgumentException from unbindService is still caught and logged with Log.wtf.
- No API changes. mComponentNameToConnectionsMap is replaced by a map that holds only the retained preferred entry.
Per major
- Android 14: not affected per the bulletin; platform/packages/modules/Nfc is not present in the android-security-14.0.0_r29 security tags.
- Android 15: not affected per the bulletin; the project is not present in the android-security-15.0.0_r17 security tags.
- Android 16: same fix: commit a90c62ca6e75 in android-security-16.0.0_r7, follow-up 6bc72fe8b34e in android-security-16.0.0_r8; 8/8 lines of the patched region present at android-security-16.0.0_r8.
- Android 17: not affected per the bulletin listing in the per-major facts; at android-security-17.0.0_r1, 7/8 lines of the patched region are present.
Unknowns
- What mAidCache.getPreferredService() returns when the preferred service is unset, which affects which entry, if any, is kept.
- How isMultipleBindingSupported() is implemented, and the default state of Flags.allowMultipleHceBindings and DONT_IMMEDIATELY_UNBIND_SERVICES in the affected releases.
- The full body of the deferral branch in unbindServiceIfNeededLocked() (the diff cuts it off).
- Which of the 8 patched lines is missing at android-security-17.0.0_r1, and why the bulletin's version list includes 17-next while the per-major facts treat 17 as not listed.
- Which releases contain the change referenced as b/365533082.
- The concrete privilege impact of a retained system-side binding (the bulletin only states High EoP).