“Der teuerste 1-Byte-Fehler”
Bei ACM ist ein lesenswerter Artikel über die Fehlentscheidung, in C Null-terminierte Strings einzusetzen, erschienen.
Genau dieses Problem und die Schwächen von C habe ich auch schon oft bemängelt. Null-terminierte Strings – und viele andere POSIX- und IEEE-Macken wie die grausliche Arithmetik – sind ein endloser Quell von Sicherheitslöchern. Wesentlich besser wäre es, die Länge von Strings als Zahl und nicht durch ein ausgezeichnetes Sonder-Byte darzustellen. Es ist unglaublich, wieviele Sicherheitsprobleme es schon durch Mehrdeutigkeiten bei Strings, die ein Null-Byte enthalten, vergessenes Nachzählen, Pufferüberläufe usw. entstanden sind. Wobei man zugestehen muß, daß das damals plausibel erschien, als jedes Byte Speicher sehr wertvoll und knapp war und man auf unterstem Niveau, aber auch ohne Angreifer programmiert hat.
Allerdings sollte meines Erachtens ein String nicht, wie in dem Artikel angesprochen, ein Tupel nur aus der Länge und dem Text sein, sondern eben Länge des zur Verfügung stehenden Speicherpuffers, Länge des Textes, Text und eigentlich auch der Zeichensatz.
Noch so ein Katastrophenfehler, wo wirklcih gepfuscht wurde, ist die UTF-8-Kodierung, bei der es für Zeichen mehrere Kodierungen gibt, die man beim Filtern leicht übersehen kann. Sowas hätte auch nicht passieren dürfen, da hätte die Kodierung eindeutig sein müssen.
Die Frage ist, was schlimmer ist: Dieser große Haufen zwangsrückwärtskompatibler Müll (einschließlich der im Artikel kritisierten Fehlentscheidung, Bill Gates mit einem Betriebssystem zu beauftragen), oder daß wir keine Alternative haben zu der wir wechseln könnten, selbst wenn wir wollten?
Schönes Zitat aus dem Artikel:
To a lot of people, C is a dead language, and ${lang} is the language of the future, for ever-changing transient values of ${lang}. The reality of the situation is that all other languages today directly or indirectly sit on top of the Posix API and the NUL-terminated string of C.
When your Java, Python, Ruby, or Haskell program opens a file, its runtime environment passes the filename as a NUL-terminated string to open(3), and when it resolves queue.acm.org to an IP number, it passes the host name as a NUL-terminated string to getaddrinfo(3). As long as you keep doing that, you retain all the advantages when running your programs on a PDP/11, and all of the disadvantages if you run them on anything else.
Schön gesagt. (Danke an den Leser für den Link!)
21 Kommentare (RSS-Feed)
Naja, alloziierten Speicher braucht man nur bei veränderbaren Strings. Und es wäre soooo schön, wenn es “den einen” Zeichensatz gäbe, und man den nicht speichern müsste.
Kleine Anekdote: Ruby hat versucht, alles auf Unicode umzustellen (so wie Python das ja auch macht). Aber der Ruby-Erfinder Matz ist ja Japaner und der hat den Plan aufgegeben, da in einer der japanischen Legacy-Codierungen der Codepunkt für Backslash und ein japanischen Zeichen das gleiche ist und man den daher nicht nach Unicode codieren kann.
Ach, die Informatik, so viele Pistolen für so wenige Knie…
@Bob: Dann brauchst Du eine Markierung, ob es sich um allozierten oder fixen Speicher handelt. Denn woher soll eine Textverarbeitungsroutine das sonst erkennen können? (Es sei denn, man hat eine Sprache mit Const-Typen, aber das bringt wieder jede Menge Gehudel…)
Ist es ein großes Problem, dass durch Manipulation des “Längenfeldes” (durch Buffer Overflow etc.) auf jeden Fall andere Speicherinhalte ausgelesen / geschrieben werden können. Und nicht wie bei -Marken nur bis zum nächsten -Byte?
@denn: Andersrum: Das Problem ist ja gerade, daß sie die Länge nicht prüfen (falls es ein Längenfeld überhaupt gibt). Oder ich hab die Frage nicht verstanden.
Ach Hadmut, diese Beiträge à la Dies-und-jenes-ist-schlecht-ich-hätte-es-aber-viel-besser-gemacht häufen sich in letzter Zeit.
Es ist nun mal wie es ist und es lässt sich (zumindest in der Sprache) nicht mehr ändern. Selbst in C kann man vernünftig programmieren, würde man nicht immer wieder das Rad selbst erfinden oder sich auf die libc verlassen. Einfach mal die GLib aus der GNOME-Ecke benutzen und man kann sich um die eigentlichen Probleme kümmern (freu mich schon auf Flames…).
Was ich sagen will: Jeder weiß um die Probleme bei einer Sprache wie C, dementsprechend sollte man dann auch programmieren. Oder gleich was anderes benutzen.
@Matthias: Stimmt nicht. Erstens geht es ja nicht darum, daß ich es besser gemacht hätte. Im Gegenteil, ich verweise ja auf solche Artikel um zu zeigen, daß das nicht nur ich so sehe.
Und es weiß eben nicht jeder um die Probleme mit C. Im Gegenteil weiß die Mehrzahl der allgemeinen Anwendungs- und Webprogrammierer eben gerade nicht darum. Zu viele Löcher und zu viele Programmierfehler gibt es dafür. Deshalb muß man es ändern.
Ignorieren hilft gar nichts.
(und solange mir die Links zu solchen Themen von Lesern zugespielt werden, kann es jedenfalls nicht allen zuviel sein).
Nie im Leben hätte die Situation mit einem anderen OS in marktbeherrschender Stellung heute anders ausgesehen. Sicheres Programmieren ist nur nötig, weil es eben Angreifer gibt; die Einsicht dazu wuchs erst mit der zunehmenden Anzahl derselben. MS hat das sogar schon recht früh erkannt, da war das Kind nur schon so in den Brunnen gefallen, dass erst mit Vista was einigermassen sicheres rauskam. Jetzt aber kann man mit einem gut konfigurierten Windows mindestens genauso sicher arbeiten wie mit jedem anderen beliebigen OS. MS kann durchaus programmieren, wie der neue IIS nachweist.
Mathias
@Mathias Hebel: Stimmt nicht. Der Stil von Microsoft ist einzigartig.
Was dazu noch sehr ärgerlich ist – in C++ gibt es genau das was du dir wünschst, Hadmut – std::basic_string enthält die Größe des Puffers, die tatsächliche Länge des Strings und die eigentlichen Daten. Nur werden selbst heute massenhaft APIs produziert, die obwohl für C++ und nicht C gedacht, nullterminierte Strings via Pointer verlangen. Und dann kommt auch noch jede zweite C++(!)-Bibliothek daher, und meint ihre eigene String-Klasse zu erfinden, obwohl es eine standardisierte gibt…
P.S.: “Ist es ein großes Problem, dass durch Manipulation des “Längenfeldes” (durch Buffer Overflow etc.) auf jeden Fall andere Speicherinhalte ausgelesen / geschrieben werden können. Und nicht wie bei -Marken nur bis zum nächsten -Byte?”
Diese Gefahr ist wesentlich kleiner, als bei nullterminierten Strings. Bei explizit angegebenen Größen ist jede Implementierung gezwungen, diese Angabe zu berücksichtigen. Bei nullterminierten Strings reicht ein Flüchtigkeitsfehler, und das Programm liest und/oder schreibt in nicht dafür vorgesehen Bereichen. Gegen aktive Manipulation des Speichers von außen hilft keine der Varianten, aber der Normalfall ist, dass einfach irgendwo eine Überprüfung mal übersehen wurde. Oder auch nur einmal vergessen wurde das letzte Byte auf 0 zu setzen.
@Mephane: Das ist aber völlig nutzlos, weil man damit keine Betriebssystemfunktionen aufrufen kann und ich keine einzige normale Bibliothek kenne, die das verwendet. Qt hat dann schon wieder seinen eigenen Kram usw. Wenn man sowas einsetzt, kann man entweder nur selbstgeschriebenen Code verwenden oder ist wieder ständig am konvertieren. Außerdem sind die Klassenfunktionen lächerlich dürftig. Also im Endeffekt kaum brauchbar.
Außerdem ist die C++-Bibliothek einfach lausig implementiert. Da eine Schleife über irgendeinen Iterator-Typ zu schreiben ist schon wieder Krampf.
Das habe ich ja gemeint, dass alle möglichen APIs trotzdem irgendwas anderes oder einfach nur nullterminierte Strings erwarten, obwohl es eine Implementierung gibt.
Und ja, die Sache mit den Iteratoren ist in der Tat lästig (vor allem prä-C++11 wo man die genauen Typen ausschreiben muss) und es fehlen oft einige simple doch offensichtliche Funktionen in den STL-Klassen (z.B. gibt es keine Funktions “contains” die bei einem Container schaut ob ein Element mit einem bestimmten Wert enthalten ist, sondern das läuft dann auf ein “if (collection.find(value) != collection.end())” hinaus anstatt z.B. “if (collection.contains(value))” zu ermöglichen. Und vieles mehr noch. Aber darum, wie gesagt, ging es hier gar nicht.
Also ich bin der Meinung, daß damals die Entscheidung null-terminierte Strings durchaus sinnvoll, berechtigt und kein Fehler war.
Wie Hadmut schon sagte. “Angriffe” wie heute gab es nicht, und wenn, dann eher “Scherze” unter Kollgen, die aber selten bösartig waren.
Außerdem war das eine der resourcenschonendsten Implementierungen. Wer jemals mit “speicherbeschränkten” Systemen (1K-4K Hauptspeicher) gearbeitet hat, weiß was ich meine. Was war das damals für ein Luxus, als man mit dem C64, Apple II+ oder Video Genie plötzlich 48k und mehr aus dem “Vollen” schöpfen konnte.
Der Fehler war eher, “alte Zöpfe” nicht rechtzeitig abzuschneiden. So wie es heute immer noch APIs gibt, die null-terminierte Strings erwarten.
@Hadmut:
.. weil man damit keine Betriebssystemfunktionen aufrufen kann…
– Mit std::string::c_str() kannst du Betriebssystemfunktionen aufrufen (jedenfalls solche, die mit 7-bit ASCII/8-bit Codepagekram encodiert sind)
… Außerdem sind die Klassenfunktionen lächerlich dürftig….
– Die C++ Bibliothek basiert auf der Idee die Algorithmen von den Containern zu trennen (Mit gemischtem Erfolg)
Zum Thema: C ist eine Systemprogrammiersprache.. Programmierer die Probleme damit haben Speicher sicher und effizient zu verwalten sollen doch bitte andere Sprachen für ihre “Probleme” benutzen. Ja, damit meine ich auch Leute die die VMs/Runtimebibliotheken von High-level Sprachen bauen.
Jede Repräsentation hat Vor-/Nachteile und die müssen für jedes Problem bewertert werden um die beste Darstellung für eine konkrete Anwendung zu finden (Das ist übrigens der Grund für die vielen verschiedenen Implementierungen und warum es in C (und C++) keinen eingebauten string typen gibt – Neben der Tatsache das es einfach keinen string datentypen in den mir bekannten/gängigen Rechnerarchitekturen gibt)
„C ist eine Systemprogrammiersprache.”
Ja, kann man so sehen. Dann ist sie aber für Anwendungen ungeeignet.
womit sich wieder die frage nach den alternativen stellt
@Hadmut: Inwiefern ist MS hier besonders?
Microsoft hat jahrelang auf Korrektheit oder sinnvolle APIs und dergleichen gepfiffen und systematisch Inkompatibilitäten, eigene Standards, undokumierte APIs gebaut. Außerdem hat Microsoft über lange Zeit hinweg richtigen Schrott programmiert. Die kamen sich zwar mal ziemlich schick darin vor, daß sie so viele Leute hatten, die in Bergen von Pizzaschachteln und leeren Coladosen lebten, aber eigentlich haben diese Leute sehr viel Softwaremüll produziert.
Inzwischen hat Microsoft das gemerkt und versucht davon wegzukommen, haben sich aber in ihrem eigenen Rückwärtskompatibiliätsgestrüpp verfangen. Und es gibt keine andere Firma, die das in diesem Ausmaß getan und damit so viel Schaden angerichtet hat.
Und in einem kontrafaktischen Szenario mit einer anderen Firma in der Position wie sie MS hatte, wäre das Deiner Meinung nach nicht passiert?
@Mathias Habel: Es gibt eine ganze Menge von Firmen, mit denen das nicht oder nicht so passiert wäre. Vieles davon war schon Bill Gates spezial und nicht zwangsläufig.
Eine Ergänzung: Prinzipiell gebe ich Dir Recht, was MS und seinen Schrottcode angeht, den sie in der Vergangenheit produziert haben. Nur glaube ich nicht, dass die Vergangenheit anders und besser verlaufen wäre, hätte es MS nicht oder nicht in dieser Stellung gegeben. Zudem denke ich, dass MS es noch schaffen wird, sich aus dem selbst verursachten Schlamassel aus schlechtem Code und schlechten Konzepten zu befreien. Mit dem Win 7-Kernel sind sie beispielsweise auf einem guten Weg. Keiner der großen Konkurrenten (Apple, Linux) kann für sich noch in Anspruch nehmen, technisch überlegen zu sein, was Qualität und Sicherheit des OS angeht.
Kann ich Dir nur zustimmen.
100% ACK (Erster Kontakt mit C ’85”)
und 100% ACK zu Bill, QDOS gekauft, Gates
Gruß BA