Der Patch, der Bytes umschrieb, die er nie anfassen sollte

Die Anfrage ist bewusst winzig: Benennen Sie eine Variable in einer Parser-Datei um, die schon ISO-8859-1-kodiert war, bevor Sie ins Team kamen. Der Agent meldet Erfolg, der Diff sieht wie ein einzelner Hunk aus, und Sie öffnen die Datei. Ihr Editor zeigt é dort, wo vorher é stand — auf Zeilen, die Ihre Anfrage nie erwähnt hat. git diff --stat sagt weit mehr geänderte Zeilen aus, als Sie wollten. Nichts hat einen Fehler geworfen, nichts hat gewarnt, und die Verifikation des Tools sagte, der Patch sei persistiert. Das ist die Geschichte zweier Schreib-Pfade, die Bytes verlieren konnten, die sie nicht einmal anfassten — und der Frage, warum das Sicherheitsnetz ihnen zustimmte.
Ein Schreib-Pfad ist nur so exakt wie sein Read
Hier ist der Teil, der leicht übersehen wird, wenn man über Datei-Tools nachdenkt: Um eine Datei zu patchen, muss das Tool sie zuerst lesen, und was dieses Read mit den Bytes macht, wird zurückgeschrieben. Zwei Reads auf diesen Pfaden waren verlustbehaftet.
Der Default-Patch-Modus ging durch den Text-Transport. mode="replace" — der Default — und der V4A-Edit-Modus dekodierten für jedes Byte hinter dem ersten 1.000-Byte-Binary-Sample den Inhalt mit errors="replace". Das ist der höfliche Decoder, den man für die Anzeige will: Jedes Byte, das UTF-8 nicht dekodieren kann, wird zu U+FFFD, dem Replacement-Zeichen. Für einen Menschen, der ein Log liest, ist das ein Gefallen. Für ein Read, das einen Write speist, ist es eine stille Mutation — jedes nicht dekodierbare Byte auf Zeilen, die Ihr Edit nie berührt hat, kam als EF BF BD wieder heraus. Ein einzeiliger Patch auf jenem Latin-1-Parser schrieb jedes 0xE9 in der Datei um.
Das V4A-Read führte ein Cleanup aus, das es nicht mehr brauchte. Der Raw-Read-Pfad schob die ganze Datei durch ein Terminal-Fence-Leak-Cleanup, das OSC-Escape-Sequenzen und BEL-Bytes entfernt und jede Zeile verwirft, die ein __HERMES_FENCE_-Literal enthält. Dieser Wrapper war ein Workaround für eine Backend-Schicht, die inzwischen ersetzt wurde; nichts emittiert ihn mehr, was bedeutet, dass das Cleanup nur noch Bytes entfernen konnte, die zu Ihrer Datei gehörten — Steuerzeichen in einem Fixture, einem Capture, einer Datei mit Legacy-Protokoll.
Warum nichts es bemerkt hat
Das Tool hat einen Verifikationsschritt. Der gezeigte Diff auch. Beide lasen die Datei durch denselben verlustbehafteten Transport neu, der sie gerade korrumpiert hatte. Korrupte Bytes dekodieren auf dem Rückweg zu denselben Replacement-Zeichen, der Vergleich passt, und das Tool meldet einen sauberen, persistierten Patch.
Diese Kombination — beim Read mutieren, dann mit demselben Read verifizieren — ist das Muster, das man über dieses Projekt hinaus im Kopf behalten sollte. Eine Verifikation, die denselben Transform teilt, der den Schaden verursacht hat, kann ihn nicht entdecken; sie kann nur bestätigen, dass der Schaden stabil ist. Das gemeldete Symptom war deshalb nie eine Fehlermeldung. Es war ein Post-Mortem: ein Reviewer, der Mojibake im git diff bemerkt, oder eine CI, die auf einer Datei scheitert, deren Encoding sich still unter ihr geändert hat.
Was der Fix tut
PR #120515 lässt beide Patch-Modi genau die Bytes lesen, die sie zurückschreiben. Die Reads sind auf einem lokalen POSIX-Host für reguläre Dateien nativ (eine FIFO geht weiterhin über den Shell-Pfad, mit ihrem Timeout) und base64 über die Leitung für Remote-Backends. Der Inhalt überquert dann die Text-Schicht mit surrogateescape, dem Codec, der jedes nicht dekodierbare Byte verlustfrei round-trippt — er ist das exakte Inverse des Encodes, den write_file für diesen Zweck schon nutzte, festgehalten durch einen Round-Trip-Test.
Ein Detail erklärt, warum man „nicht gültiges UTF-8“ nicht einfach zum Read-Fehler gemacht hat: Die V4A-Operationen Add File, Delete File und beide Enden von Move File nutzen einen Read-Fehler als Existenzprüfung. Eine Datei mit ungewöhnlichem Encoding in einen Read-Fehler zu verwandeln hätte dazu geführt, dass das Tool meldet, eine offensichtlich existierende Datei existiere nicht. surrogateescape hält solche Dateien lesbar und byte-exakt.
Der verwandte Bug: Writes, die die Datei leerten
Der zweite Fix, PR #122012, ist auf die direkteste Weise schlimmer. Auf den Heredoc-stdin-Backends — Modal, Daytona und Vercel — korrumpierten write_file und patch das Ziel nicht. Sie ersetzten es durch eine leere Datei.
Der Mechanismus ist ein Shell-Detail: Das Heredoc war nur an das letzte Kommando im Wrapper-Skript gebunden (trap - EXIT), sodass das cat > "$tmp", das den Inhalt bekommen sollte, überhaupt nichts las, und die leere Temp-Datei an ihren Platz verschoben wurde. Der Fix leitet das Heredoc auf das ganze Compound-Kommando um und ergänzt die fehlende Hälfte, damit der Body byte-exakt ankommt — ohne den abschließenden Newline, den das Heredoc hinzufügt, damit Writes verifizieren.
Der Follow-up-Commit ist eine kleine Meisterklasse in Shell-Portabilität: Die Payload wird nur mit Shell-Builtins neu ausgegeben, wobei base64/head bewusst vermieden werden, weil ein fehlendes Binary dem Kommando still wieder leeren stdin geben würde; und || : ist nötig, weil bei vererbtem set -e der End-of-File-Status von read den Reader killte und leeren stdin produzierte, gemessen auf bash 3.2 und 5.2. Ein Bug, der Dateien aus der Ferne leert, wurde behoben, ohne eine einzige externe Abhängigkeit zum kritischen Pfad hinzuzufügen.
Was Sie dagegen tun können
- Kennen Sie das Encoding, bevor Sie patchen.
file -I path/to/filedauert eine Sekunde und sagt Ihnen, ob Sie in UTF-8 oder in einem Legacy-Single-Byte-Encoding unterwegs sind. Alles, was nicht UTF-8 ist, verdient die folgenden Checks. - Verifizieren Sie mit Bytes, nicht mit dem Tool.
git diff --stat(Zeilenzahlen, nicht „sieht gut aus“),cmpgegen eine bekannt gute Kopie, oder ein Hash der Region, die Sie nicht angefasst haben. Bei binärnahen Dateien — Fixtures, Captures, Vendor-Blobs — ist ein Hash der einzige ehrliche Check. - Betrachten Sie Post-Edit-Verifikation als fragwürdig, wenn sie vom selben Tool kommt. Wenn das Tool, das die Datei geschrieben hat, Ihnen auch sagt, die Datei sei in Ordnung, ist das eine Aussage über seinen eigenen Read-Pfad, nicht über Ihre Bytes.
- Prüfen Sie alles nach, was in den letzten Wochen auf ein Sandbox-Backend geschrieben wurde. Eine Zero-Byte-Datei meldet sich als Datei; wenn eine Config, eine Lockfile oder ein Fixture mysteriös leer war, ist das eine mögliche Erklärung. Leer ist leichter zu entdecken als korrupt — aber nur, wenn Sie hinsehen.
- Beide Fixes liegen nur auf
main. Sie merged am 25. September 2026, einen Tag nach dem Tag v0.21.5, sind also nicht in diesem Tag (was er enthält, steht auf der Release-Seite v0.21.5). Sie kommen mit dem nächsten Release oder einemhermes update, dasmaintrackt. Wenn Sie heute auf den Hosted- oder Sandbox-Pfaden unterwegs sind, gewöhnen Sie sich an, nach einem Write die Dateigrößen zu prüfen.
Encoding ist ein wiederkehrendes Thema in den jüngsten Fixes dieses Projekts — der Writeup zum Windows-Locale-Fix behandelt die andere Hälfte, bei der der Crash vom Dekodieren statt vom Schreiben kam. Und die allgemeine Frage „sind meine Daten wirklich intakt?“ wird ausführlicher in den Notizen zur State-DB-Korruption behandelt: gleiche Regel, andere Speicherschicht.
Was Sie mitnehmen sollten
Der interessante Fehler hier ist nicht, dass eine Datei korrumpiert wurde. Es ist, dass die Verifikation denselben Fehler teilte. Ein Check, der durch denselben Transform, Codec oder Transport läuft wie die Operation, die er prüft, kann immer nur Konsistenz bestätigen — nie Korrektheit. Wenn Sie eine Pipeline bauen, die Daten liest, transformiert und schreibt, die nicht von Ihnen stammen, behalten Sie mindestens einen unabhängigen Check, der den Transform nie berührt: eine Byte-Zählung, ein Hash, eine Größe, eine Kopie, die vor dem Write angelegt wurde. In diesem Fall ist dieser unabhängige Check genau das, was der Fix wieder möglich gemacht hat, und er ist der Grund, warum die nächste stille Korruption dieser Art als Diff auffallen wird statt als Überraschung.