Una funzione restituisce false. Il compilatore non segnala errori, il programma viene collegato e parte. Dall’altra parte del confine tra Rust e C, però, il risultato può diventare 16. Non è un esempio inventato per spiegare quanto siano pericolosi i linguaggi di basso livello: è il caso concreto che ha portato a una delle correzioni di Rust 1.99.1, pubblicato il 15 ottobre 2026.
Si potrebbe liquidare la notizia come una patch minore. Sarebbe comodo, perché ci permetterebbe di continuare a parlare di linguaggi “sicuri” come se bastasse scegliere la sintassi giusta per ottenere un sistema affidabile. Il punto interessante, invece, è esattamente il contrario: un sorgente corretto non garantisce che il contratto tra componenti venga rispettato. E quando il contratto si rompe sotto il livello del linguaggio, i test che sembravano rassicuranti possono diventare inutili.
Un booleano non è soltanto vero o falso: è anche un contratto binario
La segnalazione #163911 contiene una riproduzione minima con una funzione Rust esposta tramite extern "C" e richiamata da C. L’espressione Rust calcola correttamente un bool, ma con Rust 1.99.0 il chiamante può osservare un valore grezzo diverso da 0 o 1. Nell’esempio riportato dall’autore, il programma stampa 16.
Qui entra in gioco l’ABI, Application Binary Interface: non la firma leggibile di una funzione, ma l’accordo concreto su registri, bit, parametri e valori di ritorno. Su x86_64, per quel tipo di ritorno booleano, i bit da 1 a 7 devono essere azzerati. Nella regressione il compilatore aveva smesso di esprimere correttamente quel vincolo nel codice intermedio LLVM. Il valore logico rimaneva sensato dentro Rust; la rappresentazione osservabile all’esterno no.
La differenza è minuscola nel disassemblato e enorme nella diagnosi. Il report confronta il codice generato da due versioni: in quella precedente compare un’operazione di mascheramento, in quella difettosa manca. Il compilatore ha prodotto codice macchina apparentemente valido, ma non conforme all’interfaccia attesa dal chiamante. Nessun messaggio di errore al build, nessun crash obbligatorio, nessuna garanzia che i test interni al crate se ne accorgano.
La sicurezza di un linguaggio finisce dove smettiamo di verificare i contratti con ciò che gli sta intorno.
Questo non smentisce le garanzie di Rust su memoria e ownership: le colloca nel loro perimetro reale. Memory safety, correttezza del codegen e conformità ABI sono problemi collegati, non sinonimi. È proprio quando si mescolano queste proprietà in un unico slogan che si perde la capacità di capire dove cercare un difetto.
Il dettaglio più istruttivo: il test esisteva, ma guardava il target sbagliato
Nella discussione tecnica emerge un particolare più utile di molte dichiarazioni sulla qualità: il test introdotto per controllare il trattamento dei booleani non esercitava x86_64-unknown-linux-gnu, il target su cui il problema era riproducibile, ma x86_64-unknown-uefi, che aveva un comportamento diverso. Non mancava semplicemente un test; mancava la corrispondenza fra il contratto da proteggere e l’ambiente su cui lo si verificava.
Per un team che integra C, Rust e altre librerie native, questa distinzione cambia il modo di progettare la CI. Un test unitario in Rust controlla la semantica della funzione; un test di integrazione che attraversa davvero il confine FFI controlla anche ciò che riceve il consumatore. Servono entrambi. E serve farli girare sui target e sulle toolchain che distribuiamo, non soltanto su quelli comodi per il runner.
L’issue è stata classificata P-critical dal progetto Rust e la correzione ha ripristinato l’estensione a zero richiesta dall’ABI. La nota ufficiale della release 1.99.1 conferma il fix. Non è una dimostrazione che ogni applicazione compilata con 1.99.0 sia difettosa: l’esposizione dipende dall’uso concreto dell’FFI, dall’architettura e dal percorso di compilazione. Ma chi distribuisce librerie native con quel profilo deve valutare una ricompilazione e un test incrociato, non limitarsi ad aggiornare il compilatore della propria macchina.
UEFI e wcslen: un’ottimizzazione può aggiungere una dipendenza che non avevi chiesto
Il secondo difetto sembra più semplice perché termina con un errore di linking. Su alcuni target, in particolare UEFI, mancava il simbolo wcslen. Il pass LLVM loop-idiom-wcslen poteva riconoscere un pattern e sostituirlo con una chiamata a quella funzione; il target, però, non disponeva necessariamente di una libc capace di fornirla. Risultato: un simbolo introdotto dall’ottimizzazione e assente al collegamento.
La situazione era particolarmente scomoda perché, secondo le release notes ufficiali, i progetti UEFI che usavano std risultavano compromessi: alcune chiamate erano già finite nella libreria standard precompilata. Il report #163614 mostra un caso concreto di build UEFI che falliva dove una versione precedente funzionava.
La patch aggiunge un’implementazione di ripiego in compiler-builtins. La lezione, tuttavia, non riguarda soltanto quella funzione: il compilatore può cambiare le dipendenze effettive del binario anche quando il codice sorgente non menziona affatto una certa API. È un problema importante nei sistemi embedded, nei firmware e negli ambienti senza libc completa. Un target nominalmente supportato non è una prova che tutte le combinazioni di ottimizzazione e librerie siano coperte.
macOS e rust-lld: funzionava con rustup, si rompeva senza
La terza regressione tocca la distribuzione della toolchain. Su macOS, nel caso documentato per aarch64-apple-darwin, rust-lld poteva non trovare libLLVM.dylib quando rustc veniva invocato direttamente dal percorso del sysroot, anziché attraverso il proxy rustup. Il report #163947 include una riproduzione con compilazione di una libreria WebAssembly e mostra il caricamento fallito della libreria dinamica.
Il proxy impostava condizioni ambientali che mascheravano il problema. Chi usava la toolchain attraverso percorsi diversi, per esempio strumenti che fissano un sysroot e chiamano direttamente il compilatore, poteva invece incontrarlo. La correzione inserisce nell’eseguibile rust-lld un rpath verso la directory effettiva di libLLVM.dylib.
È un promemoria utile per chi automatizza le build: non basta testare il comando che funziona sulla postazione di sviluppo. Occorre verificare il modo in cui il compilatore viene invocato nell’ambiente di consegna. È un problema vicino a quello che emerge quando si cambia linker mantenendo la promessa di compatibilità: nel caso di Mold 3.0 e la riscrittura in Rust, il valore operativo non era soltanto cambiare implementazione, ma non sorprendere il software che dipendeva dal comportamento precedente.
Che cosa dovrebbe cambiare in una pipeline reale
Tre bug, tre manifestazioni differenti: un valore sbagliato senza errore evidente, un link che non riesce, un linker che non parte. Il denominatore comune non è un difetto generico di Rust. È che la verifica del solo codice applicativo lascia scoperti i confini del sistema. Se vogliamo ridurre il rischio, i controlli devono assomigliare di più al modo in cui il software viene consumato.
- FFI: aggiungere test con chiamanti compilati separatamente, anche in C, che verifichino valori di ritorno e convenzioni ABI sui target effettivi.
- Target: eseguire build e linking per UEFI/embedded o altri ambienti con runtime ridotto, includendo i profili ottimizzati realmente distribuiti.
- Toolchain: testare sia l’invocazione attraverso i wrapper abituali sia quella diretta quando è prevista da CI, packaging o distribuzioni alternative.
- Artefatti: registrare versione del compilatore, target triple, linker e opzioni di build; valutare la ricompilazione dei binari esposti alla regressione invece di considerare sufficiente un aggiornamento locale.
- Regressioni: conservare riproduzioni minime e confrontare gli output osservabili dal consumatore, non soltanto i test interni al modulo.
Questo non significa trasformare ogni progetto in un laboratorio di compilatori. Significa scegliere i confini più rischiosi e testarli in modo rappresentativo. Un servizio web che non usa FFI nativa ha priorità diverse da una libreria esportata verso C; un’applicazione Linux ordinaria non ha la stessa matrice di un firmware UEFI. La strategia migliore è quella che esplicita le dipendenze reali e misura il rischio, non quella che aggiunge job alla CI per poter mostrare un numero più grande.
Una patch release non è piccola quando corregge il contratto
Rust 1.99.1 non introduce una nuova caratteristica da mettere in copertina. Corregge tre regressioni che toccano generazione del codice, librerie di supporto e distribuzione del linker. Ed è proprio questo il motivo per cui merita attenzione: il valore di una release non si misura dal numero di feature, ma da ciò che torna a essere affidabile.
Il caso Ubuntu 26.10 e della filiera software racconta un problema affine da un’altra prospettiva: adottare componenti scritti in Rust non rende automaticamente sicuro il processo che li costruisce, integra e distribuisce. Qui vediamo lo stesso principio a un livello ancora più basso, nel passaggio dal codice al binario.
Per aggiornare la toolchain stable installata tramite rustup, il comando indicato dal team è rustup update stable. Per chi produce librerie native o artefatti cross-target, il lavoro non finisce lì: servono ricompilazione dove pertinente, test e verifica dei consumatori. Perché il contratto più pericoloso non è quello che fallisce rumorosamente. È quello che sembra rispettato finché qualcuno non lo usa davvero.
Domande frequenti
Che cosa corregge Rust 1.99.1 rispetto a Rust 1.99.0?
Corregge tre regressioni: il simbolo wcslen mancante su alcuni target, la restituzione di bool attraverso FFI su x86_64 e il caricamento di libLLVM da rust-lld su macOS in alcune modalità di invocazione.
Perché un bool corretto in Rust poteva diventare 16 in C?
Il compilatore poteva omettere la zero-extension richiesta dall’ABI x86_64 per il ritorno booleano, lasciando bit del registro non azzerati. Il chiamante esterno poteva così osservare un valore grezzo diverso da 0 o 1.
Rust 1.99.1 dimostra che Rust non è memory-safe?
No. La regressione riguarda la generazione di codice e il contratto ABI tra componenti, non l’abolizione delle garanzie di memoria e ownership offerte dal linguaggio nel loro ambito.
Aggiornare rustup basta per correggere tutte le librerie già distribuite?
L’aggiornamento corregge la toolchain locale. Per librerie native e artefatti già compilati con una versione coinvolta occorre valutare la ricompilazione, il rilascio aggiornato e test con i consumatori effettivi.
