
Backport di tre correzioni f_hid pubblicate (incl. CVE-2026-31721, CVE-2026-31606) a un kernel vendor Android Linux 4.14.190 EOL, con registrazioni di verifica sul dispositivo.
Tre correzioni a drivers/usb/gadget/function/f_hid.c, portate a mano su un kernel vendor Android 4.14.190
e verificate sul dispositivo su cui girano.
4.14 è a fine vita. Non esiste alcun backport stabile per nessuna di queste, e non arriverà. È proprio questo il motivo per cui esiste questo repository: il dispositivo è mio, gira un kernel che nessuno mantiene, e uno di questi difetti lo stava riavviando.
Io non ho scoperto queste vulnerabilità. Tutte e tre sono pubblicate, con patch upstream scritte da altre persone, accreditate sotto. Quello che ho fatto io:
Il punto 3 è la parte che chiederei a un lettore di valutare. Due di queste sono confermate tramite misurazione rispetto a una baseline. La terza non lo è, e non può esserlo, su questo dispositivo. Questa distinzione è mantenuta per tutto il testo piuttosto che attenuata.
| # | Patch | Autore upstream | Stato sull'hardware |
|---|---|---|---|
| 1 | Ciclo di vita di f_hidg vs cdev (use-after-free) | John Keeping, 2022-11-22 | ✅ efficacia misurata |
| 2 | CVE-2026-31721 — stato del ciclo di vita dell'oggetto re-inizializzato in hidg_bind | Michael Zimmermann, 2026-03-31 | ✅ efficacia misurata |
| 3 | CVE-2026-31606 — cdev_init su un cdev in uso | Michael Zimmermann (81ebd43cc0d6d), 2026-03-27; follow-up -ENOMEM di Ethan Tidmore, 2026-04-02 | ⛔ non osservabile su questo dispositivo |
f_hidg vs cdevf_hidg_open memorizzava il f_hidg * senza acquisire alcun riferimento. f_hidg_poll registrava i waiter
di poll su code di attesa incorporate nel f_hidg allocato con kzalloc. hidg_unbind chiamava
cdev_del(), che non invalida i fd aperti, e hidg_free poi liberava l'oggetto con kfree. Nulla ne
gestiva il conteggio dei riferimenti rispetto ai file aperti — una grep sull'intero file per
refcount|kref|atomic_|open_count trovava solo opts->lock.
Quindi un loop select()/poll() su /dev/hidg0 che sopravviveva alla ricomposizione del gadget da parte
di Android avrebbe chiamato remove_wait_queue() su slab liberato, acquisito spin_lock_irqsave su una
parola ticket spazzatura, e girato in loop con gli IRQ mascherati per sempre. Il timer locale si ferma, il
thread di pet del watchdog non viene mai svegliato, e il watchdog non sicuro abbaia ~11 s dopo. In pratica:
il telefono si riavvia da solo. L'UsbDeviceManager di Android ricompone da solo — misurato due volte a
~550 ms — senza alcuna azione dell'utente, quindi non serviva alcun trigger esotico.
Correzione upstream: "usb: gadget: f_hid: fix f_hidg lifetime vs cdev", John Keeping, 2022-11-22,
Fixes: 71adf1189469. Il backport è pulito — ogni primitiva esiste a 4.14.190 — tranne il
kfree(hidg->set_report_buf) della patch, un campo che questa versione non ha, che viene eliminato.
CVSS 3.1 5.5, vettore AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H. hidg_bind re-inizializzava stato
appartenente al ciclo di vita dell'oggetto, non a quello del bind: due spinlock, due teste di code di
attesa e una testa di lista, nessuna delle quali appare in hidg_alloc. init_waitqueue_head reimposta
la testa di lista a puntare a sé stessa, quindi un waiter poll/select registrato prima di un rebind viene
silenziosamente orfano, e il suo poll_freewait → remove_wait_queue trova poi prev->next errato.
Upstream sposta nove inizializzazioni fuori da hidg_bind; solo cinque di esse esistono a 4.14.190.
⚠ Diversi advisory vendor intitolano questa "privilege escalation". Il vettore è C:N/I:N/A:H —
solo disponibilità, cioè un denial of service locale. Lo segnalo perché la lettura più spaventosa avrebbe
giustificato una priorità che il vettore non supporta, e nessuno chiede a una pagina vendor di dimostrarsi.
Questa patch va anche un passo oltre upstream. Spostare le inizializzazioni fuori da hidg_bind lascia due
store non bloccati su campi protetti da lock, mascherati oggi solo perché il lock veniva re-inizializzato
immediatamente sopra di essi. Una volta che il lock è persistente la race è reale: uno scrittore bloccato
nel loop di attesa di f_hidg_write, su un fd che è sopravvissuto all'unbind, può acquisire il lock,
superare il controllo e leggere hidg->req mentre bind memorizza NULL al di fuori di esso — il
ricontrollo arriva dopo la dereferenziazione. Quindi la patch prende write_spinlock attorno a entrambi
gli store.
La forma generale vale la pena di essere enunciata: spostare un'inizializzazione fuori da bind può creare una race che la re-inizializzazione stava nascondendo. Chiediti cosa stava accidentalmente proteggendo l'init rimossa.
CVSS 5.5, stesso vettore. struct cdev cdev è incorporata in struct f_hidg, e hidg_bind chiamava
cdev_init() su di essa — che è memset(cdev, 0, sizeof *cdev) più kobject_init — immediatamente
prima di cdev_device_add. Un fd aperto mantiene un riferimento su quello stesso kobject tramite
chrdev_open → cdev_get → kobject_get_unless_zero. Su unbind→rebind con un fd aperto, bind cancella
stato di riferimento vivo su un oggetto referenziato.
La correzione è un cambio di struct piuttosto che una guardia: la struct cdev incorporata diventa un
struct cdev * con una cdev_alloc() per bind. cdev_init allora appare zero volte nel file — non
rimane alcun oggetto in uso da re-inizializzare.
⛔ Questo dispositivo non può rilevare questo difetto, prima o dopo la correzione. Vedi
verification/03-cve-2026-31606.md. Questa è un'affermazione positiva sulla strumentazione del dispositivo,
non una scappatoia.
Metodo e registrazioni per difetto in verification/. In breve: le correzioni 1 e 2
sono state chiuse con regressione a variabile singola rispetto alla build immediatamente precedente, con
controlli positivi, e la correzione 2 è stata confermata una seconda volta da una diversa fonte di log.
La correzione 3 è supportata solo dalla lettura del sorgente.
Le patch sono output di git format-patch contro un albero vendor Samsung 4.14.190, in ordine:
git am patches/0001-*.patch patches/0002-*.patch patches/0003-*.patch
Sono cumulative e vanno applicate in sequenza — la 3 presuppone la struct come la 1 l'ha lasciata.
GPL-2.0, come opere derivate del kernel Linux. Vedi LICENSE. La paternità upstream delle correzioni originali è accreditata sopra; il backporting, il locking aggiunto nella patch 2 e il lavoro di verifica sono miei.
Qui non vengono distribuiti artefatti compilati — nessuna immagine del kernel, nessun modulo, nessun pacchetto flashabile. Questo repository è solo sorgente ed evidenza.