Vuilnisman overbodig voor waterdicht geheugen

Reading time: 7 minutes

Author:

Mozilla, de organisatie achter de Firefox-browser, werkte de afgelopen jaren aan een nieuwe programmeertaal, Rust genaamd. De grote belofte van de taal: de prestaties van C-software zonder geheugenfouten.

Wie de beschrijvingen van softwarepatches leest, komt met de regelmaat van de klok termen tegen als buffer overflow, double free en dangling pointer. Fouten met het geheugenbeheer staan met stip boven aan de lijst met oorzaken van beveiligingsproblemen in software. En als ze niet voor een beveiligingsprobleem zorgen, kunnen ze crashes en andere bugs veroorzaken.

Het is dus niet zo gek dat programmeertalen als C#, Java, Javascript en Python kiezen voor automatisch geheugenbeheer. Maar dit kan weer grote impact hebben op de prestaties of het geheugengebruik van een systeem. Automatisch geheugenbeheer is daarmee een van de vele splijtzwammen in de software-industrie, met uitgesproken voor- en tegenstanders.

Van tijd tot tijd duikt er daarom een wel een aanpak op die de voordelen van beide technieken probeert te combineren, de een wat serieuzer dan de ander. De programmeertaal Rust is zo’n serieuze poging. De ontwikkeling ervan staat onder leiding van Mozilla, de organisatie achter de Firefox-webbrowser. Na drie jaar serieus sleutelen verschijnt half mei de 1.0-versie, waarmee Mozilla de taal voldoende uitgekristalliseerd acht om ermee aan de slag te gaan.

De belangrijkste eigenschap van Rust is ‘memory safety without garbage collection’. Rust-software elimineert fouten in geheugenbeheer, maar dat gaat gepaard zonder overhead en de programmeur houdt het roer stevig in handen. Mozilla heeft het dan ook over een systeemtaal; ook low-level systeemsoftware kan met Rust worden geschreven.

Veiligheidsproblemen door bugs in het geheugenbeheer, Mozilla wil er eigenlijk van af. De organisatie hoopt dat haar programmeertaal Rust de oplossing biedt. Of het zover komt, moet echter de komende jaren blijken.

Om het doel van memory safety te verkrijgen, zijn maatregelen op verschillende vlakken nodig. Die zijn niet allemaal nieuw. Bufferoverflows worden bijvoorbeeld bestreden met standaard klassen voor constructies als arrays, die toegang tot een element checken. Deze aanpak kan ook worden toegepast in traditionele talen – met de kanttekening dat de aanpak in Rust niet meer optioneel is.

Wat de taal bijzonder maakt, is de eigenschap dat een geheugenlocatie altijd een expliciete eigenaar moet hebben. Dat moet alle veelvoorkomende problemen oplossen die kleven aan een van de fundamentele componenten van C-gebaseerde talen: pointers. Het is bijna ondenkbaar om in een dergelijke taal software te schrijven die niet ergens een verwijzing naar een geheugenadres gebruikt om grote datastructuren uit te wisselen of efficiënte dynamische lijsten te maken, om maar wat te noemen.

Het probleem is echter dat er geen garanties zijn dat het geheugen waar een pointer naar verwijst nog geldig is. Een veelvoorkomende fout is dat een functie een niet-lokale pointer naar een lokale variabele laat verwijzen. Als de functie afloopt, gaat de variabele out-of-scope en wordt het geheugen teruggeclaimd en mogelijk hergebruikt voor wat anders. De pointer is een dangling reference geworden.

Het kan ook gebeuren dat een object verhuist in het geheugen, bijvoorbeeld als het een dynamisch uitbreidbare lijst is die niet meer in zijn vooraf geclaimde ruimte past. Een pointer heeft geen weet van zo’n verhuizing en blijft vrolijk naar het oorspronkelijke adres verwijzen. Talen met geheugenbeheer staan om dit soort redenen doorgaans geen pure pointers toe. In plaats daarvan gebruiken ze referenties die constant door een virtuele machine of runtime in de gaten worden gehouden. Met alle overhead van dien.

Fysieke wereld

Rust pakt het probleem anders aan. De taalspecificatie stelt simpelweg dat er elk moment slechts een enkele variabele mag zijn die een geheugenlocatie in bezit kan hebben. Alleen die variabele kan de gegevens lezen of schrijven. De oorspronkelijke variabele waarmee het geheugen is gedeclareerd, wordt bestempeld als de eigenaar (ongeïnitialiseerde pointers zijn verboden). Wanneer hiernaar een pointer wordt aangemaakt, leent deze de geheugenlocatie tijdelijk, totdat die weer out-of-scope gaat. En wanneer een functie wordt aangeroepen met de variabele, wordt het eigenaarschap weggegeven. Dit kan zo lang doorgaan als nodig is.

Volgens de ontwikkelaars van Rust is dit conceptueel vergelijkbaar met de fysieke wereld: je kunt een boek net zo vaak uitlenen en doorgeven als je wilt, maar je kunt er alleen iets mee doen op het moment dat je het in bezit hebt (hoewel Rust de uitzondering maakt dat een uitgeleende variabele gelezen mag worden zolang overtuigend kan worden aangetoond dat deze niet van waarde is veranderd).

De auteurs waarschuwen wel dat het concept even wennen is. Nieuwkomers wringen zich in het begin soms in allerlei bochten om de borrow checker te laten doen wat ze willen. Maar er is een goede reden om het toch te leren: de regels zijn volledig statisch te controleren zonder de software uit te voeren. Daardoor kan dit helemaal tijdens de compilatiestap worden afgehandeld. Met andere woorden: tijdens de uitvoer zijn er geen controles nodig. Ziedaar de reden voor de claim dat het waterdichte geheugen zonder overhead gepaard gaat.

De Mars Global Surveyor-satelliet hield het langer uit dan verwacht, maar kwam in 2007 toch aan zijn einde door een software-update die een foutieve verwijzing naar een geheugenadres bevatte. De desbetreffende geheugenlocatie kreeg een verkeerde inhoud, wat er uiteindelijk toe leidde dat de zonnepanelen wegdraaiden van de zon en de satelliet zonder stroom kwam te zitten. Illustratie: NASA/JPL-Caltech

Bekende methode

Dit eigenaarsmechanisme biedt een sluitende bescherming voor normale variabelen, maar kan ook worden ingezet voor expliciet vrijgemaakt, heap-gebaseerd geheugen (malloc of new in C/C++). Anders dan de normale stackvariabelen blijft dit in een traditionele programmeertaal net zo lang intact tot de programmeur het weer vrijgeeft.

Dat gaat geregeld mis. Wanneer de programmeur vergeet om geheugen vrij te geven, bijvoorbeeld omdat een conditionele vertakking over het hoofd wordt gezien, is het geheugen ‘lek’; het programma blijft alsmaar meer geheugen opeisen. Gevaarlijker is als hetzelfde stuk geheugen per ongeluk meerdere malen wordt vrijgegeven; zo’n double free kan leiden tot crashes of beveiligingsproblemen. Ook de use after free, wanneer een programmeur een vrijgegeven geheugenlocatie alsnog gebruikt, is een beveiligingsprobleem. Voor goed geheugenbeheer moet de programmeur een uitgebreid huishoudboekje bijhouden dat zelfs in alle vertakkingen, conditionele situaties en exceptions correct blijft. Een foutgevoelige en tijdrovende rotklus.

Dit lost Rust op door het heap-geheugen alleen toegankelijk te maken als boxed pointer, ofwel geabstraheerd via een object dat uitgaat van datatype en -lengte en dat de toegang controleert. Dit is een bekende methode die ook in veel andere talen voorkomt, maar gekoppeld aan het eigenaarschapsmodel van Rust biedt het ook op dit gebied sluitende bescherming zonder runtime controles.

Zo’n object volgt namelijk verder de levenscyclus van een normale variabele. Het geheugen wordt automatisch vrijgegeven als het object out-of-scope raakt (of wanneer de variabele handmatig wordt vernietigd). Use after free, double free of geheugenlekken zijn daardoor niet meer mogelijk.

Er is nog een aspect dat hiervan profiteert: Rust is uitermate geschikt om parallelle code te schrijven. Het eigenaarsmechanisme garandeert dat er geen data races kunnen ontstaan, waarbij twee gelijktijdige taken hetzelfde stuk geheugen proberen te veranderen.

Het eigenaarsmechanisme van Rust maakt het makkelijk om multithreading op een goede manier te doen. De Servo-browser-engine weet parallellisme dan ook goed uit te buiten.

Symbiotische relatie

De eerste ideeën voor Rust stammen al van 2009, als ideetje van Mozilla-werknemer Graydon Hoare. Al snel schaarde zijn werkgever zich achter het initiatief. De reden hiervoor ligt voor de hand: het overgrote deel van de beveiligingsproblemen in Firefox (en andere browsers) is te wijten aan bugs in het geheugenbeheer. Er is dan ook een ‘symbiotische relatie’ met het Servo-project van Mozilla, een experimentele browser-engine volledig geschreven in Rust. Dit is tevens het grootste project voor de nieuwe programmeertaal tot nog toe, met op het moment zo’n 133 duizend coderegels.

Het eigenaarschapsmodel is niet geheel in isolatie ontstaan. De basis ervan werd gelegd in het C-dialect Cyclone. Rust heeft dit verder doorgevoerd. Het is echter slechts een van de vele vernieuwende features in de programmeertaal. De ontwikkelaars wilden een moderne taal maken die rekening houdt met de ontwikkelingen van de afgelopen decennia. Hoare en later de andere Rust-ontwikkelaars lieten zich onder meer inspireren door ML, C++, C#, Lisp en Ruby. Zowel imperatieve en objectgeoriënteerde als functionele ideeën komen aan bod.

Het vernieuwende is natuurlijk ook gelijk een zwakte. Nieuwkomers wordt het op deze manier wel lastig gemaakt met niet alleen een ander geheugenmodel maar ook nog een reeks andere nieuwe zaken om te leren. Bovendien kenmerkte de ontwikkeling zich steeds door ingrijpende veranderingen naarmate alle nieuwe features uitkristalliseerden. De mogelijkheden en notaties voor bijvoorbeeld pointers en objecten hebben vele iteraties doorlopen, en ook is er een tijdlang toch een optionele garbage collector geweest voor managed pointers – deze functionaliteit moet naar verluidt op een goed moment in de toekomst weer worden toegevoegd.

Nu de 1.0-lijn is vastgezet, moet het gedaan zijn met de grote veranderingen. Voor de definitieve 1.0-versie is het nu vooral zaak om de schoonheidsfoutjes glad te strijken en de documentatie op orde te brengen. Dan blijft alleen nog de vraag: happen ontwikkelaars toe, of blijft Rust een interessante maar academische poging om beter met geheugenbeheer om te gaan?