Sicurezza e verificatore di classe

L'articolo di questo mese continua la discussione sul modello di sicurezza di Java iniziata in "Under the Hood" di agosto. In quell'articolo, ho fornito una panoramica generale dei meccanismi di sicurezza integrati nella Java virtual machine (JVM). Ho anche esaminato da vicino un aspetto di questi meccanismi di sicurezza: le funzioni di sicurezza integrate della JVM. In "Under the Hood" di settembre ho esaminato l'architettura del class loader, un altro aspetto dei meccanismi di sicurezza incorporati nella JVM. Questo mese mi concentrerò sul terzo polo della strategia di sicurezza della JVM: il verificatore di classe.

Il verificatore di file di classe

Ogni macchina virtuale Java ha un verificatore di file di classe, che garantisce che i file di classe caricati abbiano una struttura interna adeguata. Se il verificatore di file di classe rileva un problema con un file di classe, genera un'eccezione. Poiché un file di classe è solo una sequenza di dati binari, una macchina virtuale non può sapere se un particolare file di classe è stato generato da un compilatore Java ben intenzionato o da loschi cracker intenzionati a compromettere l'integrità della macchina virtuale. Di conseguenza, tutte le implementazioni JVM hanno un verificatore di file di classe che può essere richiamato su classi non attendibili, per assicurarsi che le classi siano sicure da usare.

Uno degli obiettivi di sicurezza che il verificatore di file di classe aiuta a raggiungere è la robustezza del programma. Se un compilatore difettoso o un cracker esperto ha generato un file di classe che conteneva un metodo i cui bytecode includevano un'istruzione per saltare oltre la fine del metodo, quel metodo potrebbe, se fosse stato invocato, causare il crash della macchina virtuale. Pertanto, per motivi di robustezza, è importante che la macchina virtuale verifichi l'integrità dei bytecode che importa.

Sebbene i progettisti di macchine virtuali Java possano decidere quando le loro macchine virtuali eseguiranno questi controlli, molte implementazioni eseguiranno la maggior parte dei controlli subito dopo il caricamento di una classe. Una macchina virtuale di questo tipo analizza i bytecode (e verifica la loro integrità) una volta, prima che vengano eseguiti. Come parte della verifica dei bytecode, la macchina virtuale Java si assicura che tutte le istruzioni di salto, ad esempio goto(salta sempre),ifeq(salta se in cima allo stack zero), ecc. - provoca un salto a un'altra istruzione valida nel flusso di bytecode del metodo. Di conseguenza, la macchina virtuale non ha bisogno di controllare un target valido ogni volta che incontra un'istruzione di salto mentre esegue i bytecode. Nella maggior parte dei casi, controllare tutti i bytecode una volta prima che vengano eseguiti è un modo più efficiente per garantire robustezza rispetto al controllo di ciascuna istruzione bytecode ogni volta che viene eseguita.

Un verificatore di file di classe che esegue il controllo il prima possibile molto probabilmente opera in due fasi distinte. Durante la fase uno, che avviene subito dopo il caricamento di una classe, il verificatore del file di classe controlla la struttura interna del file di classe, inclusa la verifica dell'integrità dei bytecode in esso contenuti. Durante la fase due, che avviene quando vengono eseguiti i bytecode, il verificatore di file di classe conferma l'esistenza di classi, campi e metodi referenziati simbolicamente.

Fase uno: controlli interni

Durante la prima fase, il verificatore di file di classe controlla tutto ciò che è possibile per archiviare un file di classe esaminando solo il file di classe stesso (senza esaminare altre classi o interfacce). La prima fase del verificatore del file di classe si assicura che il file di classe importato sia formato correttamente, internamente coerente, aderisca ai vincoli del linguaggio di programmazione Java e contenga bytecode che saranno sicuri per l'esecuzione della macchina virtuale Java. Se il verificatore di file di classe rileva che qualcuno di questi non è vero, genera un errore e il file di classe non viene mai utilizzato dal programma.

Controllo formato e coerenza interna

Oltre a verificare l'integrità dei bytecode, il verificatore esegue molti controlli per il formato corretto del file di classe e la coerenza interna durante la fase uno. Ad esempio, ogni file di classe deve iniziare con gli stessi quattro byte, il numero magico: 0xCAFEBABE. Lo scopo dei numeri magici è quello di rendere più facile per i parser di file riconoscere un certo tipo di file. Pertanto, la prima cosa che un verificatore di file di classe probabilmente controlla è che il file importato inizia effettivamente con 0xCAFEBABE.

Il verificatore di file di classe controlla anche che il file di classe non sia né troncato né potenziato con byte finali aggiuntivi. Sebbene diversi file di classe possano avere lunghezze diverse, ogni singolo componente contenuto in un file di classe indica la sua lunghezza e il suo tipo. Il verificatore può utilizzare i tipi e le lunghezze dei componenti per determinare la lunghezza totale corretta per ogni singolo file di classe. In questo modo, può verificare che il file importato abbia una lunghezza coerente con il suo contenuto interno.

Il verificatore esamina anche i singoli componenti per assicurarsi che siano istanze ben formate del loro tipo di componente. Ad esempio, un descrittore di metodo (il tipo restituito del metodo e il numero e i tipi dei suoi parametri) viene memorizzato nel file di classe come una stringa che deve aderire a una determinata grammatica libera dal contesto. Uno dei controlli che il verificatore esegue sui singoli componenti è assicurarsi che ciascun descrittore di metodo sia una stringa ben formata della grammatica appropriata.

Inoltre, il verificatore di file di classe verifica che la classe stessa aderisca a determinati vincoli imposti dalle specifiche del linguaggio di programmazione Java. Ad esempio, il verificatore applica la regola che tutte le classi, eccetto la classe Object, devono avere una superclasse. Pertanto, il verificatore file di classe controlla in fase di esecuzione alcune delle regole del linguaggio Java che avrebbero dovuto essere applicate in fase di compilazione. Poiché il verificatore non ha modo di sapere se il file di classe è stato generato da un compilatore benevolo e privo di bug, controlla ogni file di classe per assicurarsi che le regole siano seguite.