Microsoft: Rust kan løse hukommelsesproblemer i C og C++

26. juli 2019 kl. 05:0841
Microsoft: Rust kan løse hukommelsesproblemer i C og C++
Illustration: Pixabay.com-bruger terimakasih0.
Måske er det på tide at putte gamle sprog i skraldespanden og benytte et mere moderne og sikkert system-sprog, lyder det fra software-giganten.
Artiklen er ældre end 30 dage
Manglende links i teksten kan sandsynligvis findes i bunden af artiklen.

Microsoft Security Response Centre (MSRC) har undersøgt hver eneste sårbarhed i Microsofts programmer siden 2004.

Konklusionen er klar: Langt de fleste fejl handler, som man nok ville forvente, om usikker håndtering af hukommelse i programmerne.

Men hvad hvis man helt kunne slippe for denne type fejl?

Det spørgsmål stiller Gavin Thomas, som er ledende sikkerhedschef i MSRC, i et blogindlæg.

Log ind og læs videre
Du kan læse indholdet og deltage i debatten ved at logge ind eller oprette dig som ny bruger, helt gratis.
41 kommentarer.  Hop til debatten
Debatten
Log ind eller opret en bruger for at deltage i debatten.
settingsDebatindstillinger
41
31. juli 2019 kl. 20:11

Det kan også være et bevidst valg at ikke specificere opførslen.

...hvilket vel egentligt er en større fejl end Tony Hoares "Billion Dollar Mistake", hvis man ser på hvor meget exploitable kode det har resulteret i :-)

Det gav nok OK mening back then, hvor der var noget mere flippede arkitketurer i dag, og maskinerne var væsentligt langsommere. Men det har godt nok kostet os en del... og der er så meget ID/US/UB i C/C++ at jeg er lidt mistænksom overfor personer der påstår de kan lave fejlfri kode.

40
31. juli 2019 kl. 18:47

Helt principielt vil jeg også give Troels ret i at forekomsten af undefined behavior kun kan forekomme som resultat af (mangelfuldt) sprogdesign. Standard ML har så vidt jeg ved ingen udefineret opførsel. Det er primært C og C++ der har den slags problemer,

Det kan også være et bevidst valg at ikke specificere opførslen. C specificerer ikke hvad der sker når en signed integer laver overløb, fordi sproget tillader at hardwaren repræsenterer signed integers som 2-complement, one's-complement og sign-magnitude. Man kunne have valgt at der skulle ske en "trap", men flere arkitekturer har ingen effektiv måde at gøre det på, og det skulle checkes for hver addition/subtraktion/multiplikation. Så man valgte at ikke specificerer hvad der sker. Det samme gør sig gældende for de fleste andre steder i C/C++ hvor opførslen ikke er specificeret.

39
30. juli 2019 kl. 15:39

Det er ret besværligt at fremprovokere undefined behavior i Rust. Manish Goregaokar (som er en del af rusts "core" developers) har skrevet en fin artikel om det her: Undefined vs Unsafe in Rust

Pointen er at man skal gøre sig umage hvis man skal producere undefined behaviors i (ikke-unsafe) rust, og det samme gør sig gældende for implementation defined og unspecified behaviors. Langt de fleste udviklere vil aldrig have brug for at skrive mere end en håndfuld meget overskuelige unsafe blokke, som man nemt kan sikre sig rent faktisk er sikre.

Helt principielt vil jeg også give Troels ret i at forekomsten af undefined behavior kun kan forekomme som resultat af (mangelfuldt) sprogdesign. Standard ML har så vidt jeg ved ingen udefineret opførsel. Det er primært C og C++ der har den slags problemer, og det korte og det lange er at Rust undgår/løser dem tilfredsstillende, lige som så mange andre programmeringssprog gør det.

38
30. juli 2019 kl. 15:26

Derudover er GCC sammen med LLVM de oversættere der udnytter udefineret opførsel allermest aggressivt (eksempler. Det er også mit indtryk at de fleste C-programmører ikke har forståelse for hvad der egentlig er udefineret opførsel - f.eks. regner de fleste med at signed heltal bare løber over og bliver negativ ved overflow, men det passer ikke, og det har ikke noget med maskinen at gøre.

Før C11 var det også udefineret opførsel hvis ens C-fil (eller "compilation unit" i spec-sprog) ikke sluttede med et linjeskift. Jeg har dog aldrig hørt om en oversætter der udnyttede det til noget!

37
30. juli 2019 kl. 15:06

Der er det så måske en fordel at bruge den samme compiler.

Tværtimod, det er en fordel at teste flere compilere samt flere arkitekturer. ID/US/UB er fejl i din kode, ikke i compileren – hvis du vil forbedre dine chancer for at finde fejl før din nuværende compiler introducerer en ny optimering, der genererer fejlbehæftet kode på grund af din UB, har du bedre chancer hvis du tester flere compilere allerede nu.

36
30. juli 2019 kl. 14:44

Mit gæt ville være kodebaser hvor der er en bunke ID/US/UB, hvor det ikke har bidt én i røven endnu, fordi man kun har brugt én compiler. Ny platform, ny compiler, bang.

Der er det så måske en fordel at bruge den samme compiler. Jeg må sige at det er længe siden jeg har døjet med compiler problemer (7-9-13) da jeg bruger GCC og Linux. Ja principielt er der tale om en ny platform når der kommer en ny CPU, men da antallet af arkitekturer er begrænset, så kører det fint.

35
30. juli 2019 kl. 13:56

Når programmøren flere gange bliver nævnt som en almindelig kilde til problemer, så synes jeg det er værd at bemærke, at der er gode grunde til at det er således og at de altså ikke altid skal læses som et angreb på programmørstanden:

Skriver man alene, vil man have svært ved at se dårlige mønstre i sin programmering, og deri kan der skjule sig uhensigtsmæssig opførsel.

Skriver man i grupper, bliver en del af det første problem afhjulpet af flere øjne der kigger, men til gengæld opstår et nyt, hvor een udvikler ikke nødvendigvis er i samme tankesæt som en anden, og dermed misforstår konstruktioner og bagvedliggende motivation. Det kan minimeres med god dokumentation og kommunikation, men det koster penge og er et af de første gode adfærdsmønstre der ofres på deadline-alteret.

33
30. juli 2019 kl. 07:40

Har Rust macroer???

Ja.

De tillader vilkårlige transformationer og kodegenerering, ganske som Lisp-makroer, og med nogenlunde de samme fordele og ulemper. Beregningsmæssigt er de ikke stærkere end C++ templates (begge er Turing-komplette), men de er langt mere udtryksfulde, da deres definitioner udtrykkes via ordinær Rust-kode, i stedet for C++'s sære implicitte template-metaprogrammeringssprog.

Hvad typesikre måleenheder angår har jeg fundet dette bibliotek der påstår at være zero-overhead. Det foregår dog ikke via makroer, men via almindelig parametrisk polymorfi. Det er for så vidt også godt nok, da Rust's nuværende implementering af parametrisk polymorfi gør brug af monomorfisering, hvilket har samme kodemæssige effekt som C++'s template-instantiering (nemlig en kopi af den polymorfe kode for hver type den anvendes med).

32
30. juli 2019 kl. 00:54

Har Rust macroer???

Jeg kan se type generiske metoder som i ML og andre funktionelle sprog. Og generiske typer. De minder end del om templates, men er umiddelbart ikke så kraftfulde: man kan så vidt jeg kan se ikke angive andet end typer som argument. Jeg kan derfor ikke se, hvordan man kan lave noget, som minder om boost::unit - fysiske enheder indlejret i typerne og checket compile-time med zero runtime overhead. Man får f.eks. en compile fejl, hvis man ligger en længde til en længde med en tid, fås en hastighed. Enhedsalgebra compile-time, kort og godt.

Det jeg kunne finde på nettet bar rundt på enheder runtime, ligesom forsøget på at lave det i Java. Det giver jo selv sagt et overhead.

Det som man mangler er en generisk type, som kan regne på potenser i enhederne, f.eks. m*m / s / s = m^2 s^-2 compile time.

Det er ikke lige til i Rust, da man ikke kan angive potenserne ovenfor som "template" argumenter og regne på dem compile time.

30
29. juli 2019 kl. 21:57

Det er en fin diskussion :-)

Men er der ikke nogen der bruger Rust/emb Rust og vil dele deres erfaringer ?

25
29. juli 2019 kl. 15:29

10 PAGE 20 PRINT "COMAL 80 vil genopstå!!!" 30 Goto 10

24
29. juli 2019 kl. 15:25

Der er en del forskel på macroer og templates: de sidste er et Turing-komplet, funktionelt programmeringssprog, som kører i compileren og genererer ny kode og typer undervejs. Med optimering slået til, vil man ofte få fjernet meget af koden igen, og lokkeren kan måske også fjerne ubrugte symboler; men generelt bliver alle "mellemregningerne" hængende i det endelige program.

Jeg foretrækker selv templates frem for macroer, da disse bliver meget værere, hvis de skal skulle bare en smule af, hvad templates kan. Se blot på Linux kernens list implementation: det kan gøres meget kønnere med templates.

23
29. juli 2019 kl. 15:01

Der er en del forskel på macroer og templates: de sidste er et Turing-komplet, funktionelt programmeringssprog, som kører i compileren og genererer ny kode og typer undervejs. Med optimering slået til, vil man ofte få fjernet meget af koden igen, og lokkeren kan måske også fjerne ubrugte symboler; men generelt bliver alle "mellemregningerne" hængende i det endelige program.

Jeg foretrækker selv templates frem for macroer, da disse bliver meget værere, hvis de skal skulle bare en smule af, hvad templates kan. Se blot på Linux kernens list implementation: det kan gøres meget kønnere med templates.

22
29. juli 2019 kl. 14:51

Han dekomponerer maskinkoden og kigger på hvordan han i givet fald kan udnytte sårbarheder. De opstår naturligvis som beskrevet hvis den logiske sammenhæng i programmeringssproget har huller, men så sandelig også hvis compileren generere dårlig maskinkode.

Er dette ikke primært et problem i C og C++, hvor der er så kæmpe mængder af implementation-defined, unspecified og undefined behavior at det er forbandet svært at skrive korrekt kode?

Jeg kan ikke huske at have set nogle eksempler på et compiler-fejl har genereret exploitable kode. Eksempler på at selv dygtige udviklere bliver bidt af ID/US/UB er derimod en anden sag.

Og hvis man skriver C-kode som om det var en "high-level assembly" kan man også få sig nogle overraskelser, ikke mindst på grund af hvilke optimeringer compilere (ganske tilladt af standarden) kan lave. Det har mere end én gang fjernet runtime null-checks i Linux kernen, og har ledt til privesc bugs.

20
29. juli 2019 kl. 11:58

Men hvordan det så hjælper i forbindelse med f.eks. multithreading bliver lidt mere tåget for mig. Specielt når der ikke sker checks på target.

Du kan kun lave sikre operationer. Eksempelvis kan du give ejerskab over noget data til en anden tråd. Det er så den tråd der har ansvaret for at frigive hukommelse. Den oprindelige tråd kan ikke tilgå data mere, end ikke read only, så der er ikke risiko for at data tilgås efter hukommelse er frigivet.

Alt det er sikret og tjekket inden maskinkoden bliver genereret. Derfor behøver maskinkoden ikke at indeholde tjeks for det samme.

18
29. juli 2019 kl. 11:31

Inden jeg ville hoppe på en ting som Rust, vil jeg sætte mig ind i hvad jeg smider på mit target.
Hvordan holder sproget f.eks. styr på ejerskabet. Hvad er overhead ved det og hvilke dumheder kan man lave med det.

Mantraet indenfor Rust er zero cost abstractions. Det vil sige at alt det med ejerskab med videre er noget der tjekkes af oversætteren og herefter smides det væk. Der er ingen tjeks i den generede maskinkode.

Et andet eksempel er at der ingen "null" i sproget, men det er der i maskinkoden. Fordi det er effektivt og oversætteren laver ikke fejl, så den kan godt bruge konstruktioner der er farlige for os mennesker.

17
29. juli 2019 kl. 09:39

Og der er ikke nødvendigvis en 100% garanti for, at selv om et programmeringssprog er sikret i forhold til sprogets logik og brug af memory, at maskinkoden ikke kan rumme (masser af) uhensigtsmæssigheder.

Der er bestemt mulighed for at en oversætter kan generere forkert kode, og at denne kode kan udnyttes af en angriber. Uagtet gamle travere som Ken Thompson's Trusting Trust, er der så praktiske eksempler på at dette er sket? Hvor ofte sker det ift. så mange andre sikkerhedsbrister? Jeg tror ikke det i praksis er noget det er værd at bekymre sig om. Faktisk er jeg villig til at postulere en (empirisk u-underbygget) stærkere hypotese: Det er værd at lave sproget og oversætteren mere kompliceret (som Rust) for at undgå hukommelsesfejl i den programmørskrevne kode, selv hvis øger risikoen for fejl i oversætteren der potentielt kan udnyttes. Jeg mener at gevinsten er større end risikoen.

Der er massevis eksempler på at oversættere genererer forkert kode (især med mange optimeringer slået til), men det er sjældent de er blevet udnyttet af angribere, for som regel får de programmet til at crashe før det kommer i brug. Og der er blevet udviklet beviseligt korrekte oversættere (desværre til C) som garanterer at de slet ikke har sådanne fejl i den genererede kode (under en række antagelser; læs altid hvad der står med småt).

16
29. juli 2019 kl. 09:19

Jeg synes et eller andet sted, at der mangler et aspekt i diskussionen. Et er, at forbedre og modernisere programmeringssprog, så risikoen for fejl i koden minimeres. Noget helt andet er at se tingene fra en hackers synspunkt.

Vi taler her om complierede sprog, som generere masser af maskinkode, og det er klart, at der ikke må være oplagte uhensigtsmæssigheder i f.eks. memory allokering, frigivelse, gensidig beskyttelse, raeltidsparametre osv. men typisk er det ikke kun det en hacker går efter.

Han dekomponerer maskinkoden og kigger på hvordan han i givet fald kan udnytte sårbarheder. De opstår naturligvis som beskrevet hvis den logiske sammenhæng i programmeringssproget har huller, men så sandelig også hvis compileren generere dårlig maskinkode. Og der er ikke nødvendigvis en 100% garanti for, at selv om et programmeringssprog er sikret i forhold til sprogets logik og brug af memory, at maskinkoden ikke kan rumme (masser af) uhensigtsmæssigheder.

Så uanset hvor meget man holder af et givet sprog, og uanset hvor godt sproget er til at beskytte brugeren imod fejl, så ændrer det ikke på, at man fortsat har behov for at se på den konkrete implementering af selve compileren. Er (maskin)koden sikret. Hvad sker der ved evt. krydskompilering. Hvad sker der i forskellige hardware konstellationer osv. osv. Jeg har gennem årene set skrækkelige compilere, som generere den rene skod-kode, selv om alting ser helt vandtæt og smukt ud i kildeteksten.

Så skal sproget være sikkert, så omfatter det også at selve compileringsprocessen gøres til genstand for sikkerhedsanalyse.

14
28. juli 2019 kl. 23:33

Det kan du godt hvis du aktivt tilvælger det (ved at bruge "unsafe"-blokke), men uden for disse garanterer Rust typesikkerhed og fraværet af udefineret opførsel.

Udefineret opførsel er ikke problemet. Problemet er hvor mange ukendte operationer sproget benytter for at opnå den definerede opførsel. Esben Nielsens oplevelse ovenfor et et godt eksempel. Jeg er sikker på at hans program gjorde som det skulle, hvis der var ressourcer til det.

I øvrigt er C blevet mere typesafe. Men man kan stadigt typecaste sig hele vejen til helvede.

13
28. juli 2019 kl. 22:51

Man må finde en stærkere differentiator hvis man mener at Pascal/Delphi er værd at bruge ift. f.eks. Rust.

Du gøer op ad det forkerte træ!

Jeg har ikke sagt at Pascal skulle være bedre end Rust, men kommenteret at FreePascal godt kan køre flertrådet, og at Pascal generelt har en rigtig god string håndtering.

Og så er jeg i øvrigt ret så ligeglad med om man har en eller anden up-nose holdning til at Pascal skulle være 70'er agtig, hvis det kan løse den opgave jeg vil have det til at løse, på en brøkdel af tiden med et hurtigt og stabilt resultat til følge - og det er så stadig lysår foran f.eks. MUMPS som man i region Sjælland har inficeret sygehusvæsenet med.

15
29. juli 2019 kl. 04:46

de bedste programmer er lavet i ASM :)

12
28. juli 2019 kl. 22:15

Jeg er godt klar over at det er lidt OT i forhold til programmering af embeddede systemer, men hvis man laver forretningsorienterede programmer, og/eller webapplikationer, så er der tit en farlig masse stings man skal rode med, hvorfor string håndtering så afgjort er vigtig

Det var heller ikke min pointe - strings er skam vigtige, det tror jeg alle ved. Min pointe var snarere at de er et løst problem. C er det eneste sprog hvor de stadigvæk er elendige[1]. Man må finde en stærkere differentiator hvis man mener at Pascal/Delphi er værd at bruge ift. f.eks. Rust.

[1]: Afhængigt af hvor kræsen man er har de fleste sprog dårlige strings, men så er det fordi man går op i hvorvidt de repræsenterer bytes, Unicode code points, grafemklynger, eller hvad det nu måtte være. Tekst er i almindelighed svært. Men ingen andre udbredte sprog end C har problemer med at tekstbehandling giver hukommelseskorruption.

11
28. juli 2019 kl. 21:38

Det, som laver megen ekstra, ukontrolleret kode i C++, er templates. Dem har man så vidt jeg ved også i Rust.

Jeg har selv brændt nallerne med at lave pæn C++ kode, som miksede templates og virtuelle funktioner: det gav et hav af klasser med virtuelle tabeller og runtime type-information, som fyldte alt for meget på et 16 MB stort system.

Nu koder jeg på PC størrelse systemer, og har derfor ikke den slags problemer. Jeg har gennemtvunget, at vi kun lavet een-trådede programmer, men flere af dem på Linux og på den måde bruger MMU'en til at gennemtvinge data-adskillelse. På den måde er C++ mindst lige så nemt som Java - men det oversætter meget langsommere, og værktøjerne er dårligere.

Det samme gør man iøvrigt ofte i mere embeddede systemer uden MMU, her bruger man blot besked-køer frem for pipes eller TCP forbindelser, som vi gør.

Med Rust ville jeg nok turde begynde at parellelliserer noget af koden direkte inde i hvert program, men med C++, holder jeg mig til at parellisere ved at starte flere processor op og samle igen. Det kan også bedre spredes ud på mange maskiner.

Nede i kernen, hvor parellisering er uundgåelig, er Rust nok helt klart at foretrække - men man skal nok ikke drømme om mindre hukommelsesforbrug end C++, da Rust, så vidt jeg kan se, bygger på meget af det samme.

10
28. juli 2019 kl. 21:38

Det er altså noget af en 70'er/80'er-problemstilling at gå op i strings.

Det var en mærkelig hovski-snovski holdning at udvise.

Jeg er godt klar over at det er lidt OT i forhold til programmering af embeddede systemer, men hvis man laver forretningsorienterede programmer, og/eller webapplikationer, så er der tit en farlig masse stings man skal rode med, hvorfor string håndtering så afgjort er vigtig, og det kan korte udviklingstiden voldsomt meget ned, hvis man ikke skal bøvle med trivialproblemer.

9
28. juli 2019 kl. 18:10

Jeg kan ikke se, at du ikke skulle kunne lave samme dumheder i Rust, som du kan i eks c/c++.

Det kan du godt hvis du aktivt tilvælger det (ved at bruge "unsafe"-blokke), men uden for disse garanterer Rust typesikkerhed og fraværet af udefineret opførsel. Det er altså ikke umuligt at kvaje sig i Rust, men det er mere vanskeligt, mens man samtidigt har mulighed for (bevidst) at slå sikkerhedsmekanismerne fra, hvis man har behov for at lave noget man er rimeligt sikker på er korrekt, men som bryder sprogets (konservative) regler. Man kan også bygge abstraktioner i C++ og C, men man er ikke på samme måde beskyttet mod at bryde reglerne.

Til gengæld så har Pascal rigtig god strenghåndtering, og man behøver ikke rode sig ud i et pointerhelvede.

Det er altså noget af en 70'er/80'er-problemstilling at gå op i strings. Ja, de er elendige i C. Ja, Pascal gør og gjorde det bedre. Men det har ikke noget at gøre med Rust, eller endda C++. Der er nye kampe at kæmpe! Hvis Delphi stadigvæk kæmper kampen mod C, og ikke de mange nyere sprog der har erstattet det meste C-programmering (uagtet at C stadigvæk er udbredt), så undrer det ikke at Delphi står med en fod i graven...

8
28. juli 2019 kl. 16:15

Hvad er overhead ved det og hvilke dumheder kan man lave med det.

Jeg kan ikke se, at du ikke skulle kunne lave samme dumheder i Rust, som du kan i eks c/c++.

Siden alt fra udgangspunktet er immutable (write once / read many) så er det klart nemmere at holde styr på sikkerhed og samtidighed.

Til gengæld er tradeoff jo ganske givet et højere hukommelses forbrug og mere memory allokerings overhead.

7
28. juli 2019 kl. 14:20

der er mange ting i TP/Delphi som jeg savner i C, incl. ShortString

Det kan man så let lave med C++.

Men når vi taler embedded, så er der mange som ikke ønsker C++. Forlaringen er den simple, at C++ i modsætning til C kan generere kode som laver ting bagom ryggen på udvikleren.

Inden jeg ville hoppe på en ting som Rust, vil jeg sætte mig ind i hvad jeg smider på mit target. Hvordan holder sproget f.eks. styr på ejerskabet. Hvad er overhead ved det og hvilke dumheder kan man lave med det.

6
28. juli 2019 kl. 11:24

Rust's ownership-model handler især om at gøre det rimeligt at skrive højtydende og korrekte flertrådede programmer. Der er mig bekendt ikke nogen lignende løsning i Delphi.

Det er muligt at lave flertrådede programmer i FreePascal - men jeg vil blankt indrømme at jeg aldrig har haft behov for det, da programafvikling som regel går så hurtigt at det ikke er nødvendigt med flere tråde (men ikke dermed sagt at andre ikke kan have behovet).

Til gengæld så har Pascal rigtig god strenghåndtering, og man behøver ikke rode sig ud i et pointerhelvede.

4
27. juli 2019 kl. 22:59

Rust's ownership-model handler især om at gøre det rimeligt at skrive højtydende og korrekte flertrådede programmer. Der er mig bekendt ikke nogen lignende løsning i Delphi.

3
27. juli 2019 kl. 13:04

Helt enig, der er mange ting i TP/Delphi som jeg savner i C, incl. ShortString.

2
26. juli 2019 kl. 18:28

Hej Der/vi er mange der bruger C og eks freeRTOS til embeddede systemer typisk på noget ARM.

Er der nogen der har prøvet at bruge Rust istedet (evt sammen med freeRTOS eller anden preemptive kerne) og vil dele deres erfaringer med os andre der går og overvejer det ?

Så vil jeg blive glad :-)

mvh

JEns

1
26. juli 2019 kl. 12:02

Kan vel løse det gammel kendte problem i det vilde C