Abgeschnittenes gzip: die Programmdatei, die erfolgreich heruntergeladen wurde und trotzdem beschädigt war

Ein mitten im Download abgeschnittenes gzip-Element entpackt sich zu einem kurzen, gültigen, plausiblen Dokument. Nichts wirft eine Ausnahme, und wenn Ihre Kette an einem Hash hängt, wird es das nie wieder tun.

var truncated = Gzip(Payload)[..^8];
await using var input = new MemoryStream(truncated);
await using var inflated = new MemoryStream();
await using (var gz = new GZipStream(input, CompressionMode.Decompress, leaveOpen: true))
{
    await gz.CopyToAsync(inflated);
}

Assert.Equal(Encoding.UTF8.GetByteCount(Payload), inflated.Length);

Acht Byte vom Ende einer gzip-Datei, und der Entpacker gibt die ganze Nutzlast zurück und wirft nichts. Diese Zusicherung steht in unserer Testsuite, weil die Prämisse festgeschrieben werden musste, bevor irgendetwas darauf gebaut werden konnte. Kein auf Ausnahmen gestützter Ansatz hätte das fangen können, und deshalb liest die Absicherung den Nachspann der Datei selbst, statt darauf zu warten, dass man ihr etwas sagt.

Die acht fehlenden Byte sind der Nachspann, der einzige Teil der Datei, der dem hätte widersprechen können, was herauskam.

Beschädigen Sie diese acht Byte, statt sie zu entfernen, und derselbe Entpacker wirft sofort. Ich habe an jeder der acht Nachspannpositionen ein einzelnes Bit gekippt, und jede einzelne löste eine InvalidDataException aus, ohne dass etwas geliefert wurde. Der Leser prüft den Nachspann gewissenhaft, wenn er einen sehen kann. Das Abschneiden ist gefährlich, weil es den Nachspann wegnimmt, statt ihn zu beschädigen, und eine Prüfung, die nie erreicht wird, ist dasselbe wie keine Prüfung.

Die unten beschriebene Aufnahme läuft über Programmadressen, die wir kuratieren, auf Dateien, die deren Herausgeber jedem ausliefern. Eine Programmadresse, die Sie angeben, wird auf Ihrem eigenen Gerät heruntergeladen und gelesen, und wir halten nichts von diesen Daten. Die beiden Entpacker sind dieselbe Datei, zwischen den beiden Codebasen portiert, mit im Verhältnis acht zu eins auseinandergehenden Standardobergrenzen, ein Artikel deckt also beide ab.

Ein Element, das früh endet, kann trotzdem sauber enden

Eine gzip-Datei ist eine Folge von Elementen, und jedes Element ist ein Kopf von zehn Byte mit optionalen Zusätzen danach, ein deflate-Strom, und ein Nachspann von acht Byte, der einen CRC32 und danach ISIZE trägt, die unkomprimierte Größe modulo 2^32. Innerhalb des deflate-Stroms trägt jeder Block ein Bit, das sagt, ob er der letzte Block des Datensatzes ist. Der Entpacker hört auf, wenn er dieses Bit gelesen hat und dieser Block geendet ist. Nichts in dieser Folge verpflichtet ihn, den Nachspann zu lesen, und wenn der Nachspann fehlt, gibt es nichts, was signalisieren würde, dass einer erwartet wurde. Der Strom ist zu Ende, weil der Strom gesagt hat, dass er zu Ende ist.

Der naheliegende nächste Gedanke ist, dass ein tieferer Schnitt, der in den deflate-Daten landet, den Leser wenigstens irgendwo hörbar aus dem Vorrat laufen lassen muss. Das tut er nicht, und das hat mich überrascht. Ich habe 1 bis 50 Byte vom Ende einer Datei mit einem Element entfernt, und der Entpacker gab bei jeder Tiefe wortlos eine Teilausgabe zurück: 90 Byte bei einem Schnitt von einem Byte, 67 bei einem Schnitt von zwanzig, 48 bei einem Schnitt von vierzig. Es gibt keine Tiefe, ab der die Laufzeitumgebung zu klagen beginnt.

Was den Schnitt an der Elementgrenze also zum gefährlichen macht, ist die Form der Ausgabe und nicht das Ausmaß des Schweigens. Schneiden Sie tiefer, und dem XML-Leser wird meist ein Element übergeben, das nie schließt, und er lehnt die Datei ab, und genau das sagt unser eigener Kommentar auf dem haushaltsweiten Programmweg. Schneiden Sie an der Grenze, und die Ausgabe ist Byte für Byte ein Anfangsstück des echten Dokuments, bis zu seinem letzten vollständigen Element wohlgeformt, und die beiden Felder, die ihm widersprochen hätten, sind die beiden, die noch unterwegs waren, als die Verbindung wegging. Ein so angekommener Programmführer sieht aus wie eine Quelle mit dünnen Angaben, und das ist eine andere Beschwerde als ein Programmführer, dessen Sendungen alle um eine Stunde verschoben sind, und sie verlangt eine andere Reparatur.

Schnitte, über die wir nachdenken können, kommen von HTTP, das einen Rumpf auf zwei Arten rahmt. Ein Rumpf, der kürzer ist als eine angegebene Content-Length, ist eine kaputte Antwort und der Client sagt das laut: gegen einen Socket, der 100 Byte versprach und 40 sendete, löste .NET eine HttpRequestException aus, die eine HttpIOException umschloss, "The response ended prematurely." Senden Sie dieselben 40 Byte ohne Längenangabe und schließen Sie die Verbindung, und derselbe Client gibt sie ohne Fehler zurück, denn ein per Verbindungsschluss begrenzter Rumpf ist definitionsgemäß vollständig, sofern nicht die Verbindung selbst einen Fehler gemeldet hat. Ein Proxy, der mitten im Rumpf aufgibt, erzeugt genau das: einen Strom, der endet, absichtlich, soweit irgendeine tiefere Schicht das erkennen kann.

Die Prüfung sucht eine Zahl, die sie bereits kennt

Hier ist das Ganze. Es lohnt sich, es Zeile für Zeile zu lesen, denn fast jede Zeile ist ein Zugeständnis an etwas Reales.

const int MinGzipLength = 18;
const int TailWindow = 512;

if (!input.CanSeek || input.Length < MinGzipLength || written is <= 0 or > uint.MaxValue)
{
    return;
}

var take = (int)Math.Min(TailWindow, input.Length);
var tail = new byte[take];
input.Position = input.Length - take;
await input.ReadExactlyAsync(tail, cancellationToken).ConfigureAwait(false);

var expected = (uint)written;
for (var i = 0; i + 8 <= tail.Length; i++)
{
    if (BinaryPrimitives.ReadUInt32LittleEndian(tail.AsSpan(i + 4, 4)) == expected)
    {
        return;
    }
}

var trailing = BinaryPrimitives.ReadUInt32LittleEndian(tail.AsSpan(take - 4, 4));
if (trailing > written)
{
    throw new InvalidDataException(
        "Gzip stream is truncated: the trailer reports more bytes than were decompressed.");
}

Die 18 ist ein Kopf plus ein Nachspann mit nichts dazwischen, also eine Untergrenze für "zu kurz, um überhaupt einen Nachspann zu halten", und keine Gültigkeitsprüfung. Nichts Echtes kommt dem nahe: das kleinste Element, das dieser Kodierer für ein einzelnes Byte Eingabe erzeugt, misst 21. Die Anforderung, positionieren zu können, ist die erste echte Grenze: ein Aufrufer, der geradewegs durchströmt, kann nicht geprüft werden, und die Prüfung tritt zurück, statt diese Aufrufer abzulehnen. Alle drei unserer Aufnahmewege entpacken aus einer Datei oder einem Puffer, alle drei werden also geprüft.

Der Ausstieg bei > uint.MaxValue ist die zweite Grenze, und sie ist struktureller Art. ISIZE ist die unkomprimierte Größe modulo 2^32, sobald ein Lauf also vier Gibibyte entpackt hat, lässt sich das Feld mit nichts mehr vergleichen. Das ist die Breite des Feldes, die spricht: die Prüfung schaltet sich je Lauf still selbst ab, und zwar genau für die größten Programmführer, und jede absolute Obergrenze oberhalb dieses Punktes kauft Luft, indem sie die Abschnittprüfung ausgibt.

Dann der Durchlauf. Er liest die letzten 512 Byte, geht jeden Versatz in diesem Fenster ab und liest das zweite jedes Paares von Vier-Byte-Wörtern als möglichen ISIZE. Eine Übereinstimmung irgendwo bedeutet, dass ein Element von genau der Größe, die wir entpackt haben, irgendwo in diesem Ende endet, und das ist es, was eine gefüllte Datei duldet. Das hat einen Preis: 505 mögliche Versätze gegen einen Wert von vier Byte ergeben eine Falschübereinstimmung von rund eins zu achteinhalb Millionen, wenn die Endbytes nichts mit uns zu tun haben. Das ist Rechnerei und keine Messung, und die Fenster überlappen sich, es sind also keine unabhängigen Ziehungen.

Passt kein Versatz, werden die letzten vier Byte als Zahl gelesen und verglichen. Ein Nachspann, der weniger Byte meldet, als wir geschrieben haben, passt zum letzten von mehreren verketteten Elementen, er geht also durch. Ein Nachspann, der mehr meldet, kann nicht aus einer vollständigen Datei kommen, er scheitert also. Diese Asymmetrie ist das ganze Urteil, und dort muss die Ehrlichkeit sitzen. Wenn der Nachspann fehlt, sind diese vier Byte komprimierte Daten, die als Länge gelesen werden, und die Prüfung funktioniert nur, weil ein beliebiger 32-Bit-Wert meist größer ist als die Anzahl der Byte, die wir entpackt haben. Bei 40 MB entpackt rutscht das etwa eins zu hundertsieben durch. Bei zwei Gigabyte entpackt sind es 0.47, und weil die Zahl in diesem Bruch das ist, was wir tatsächlich entpackt haben, und nicht die wahre Größe des Programmführers, stehen die Chancen am schlechtesten für den Schnitt, auf den es am meisten ankommt: den, der fast vollständig ankommt.

Die Absicherung wird also schwächer, je größer die Programmführer werden, was die falsche Richtung ist, und die größten Programmführer sind die, deren Downloads lange genug dauern, um abgeschnitten zu werden.

Zwei der drei Aufgaben erledigt die Laufzeitumgebung bereits

Die Schleife, die über verkettete Elemente läuft, trägt in ihrem Ausnahmebehandler das, was ich lange für die Fülllogik hielt.

catch (Exception ex) when (ex is InvalidDataException or EndOfStreamException)
{
    // Trailing padding after a member that ended cleanly is tolerated;
    // some upstreams append junk after the gzip trailer. Anything else
    // is a truncated download, and swallowing it would write a partial
    // guide, hash it as the content, and then skip every later fetch as
    // "unchanged" until the upstream bytes happen to move.
    if (completedMembers == 0 || guard.Written > writtenBefore)
    {
        throw;
    }

    break;
}

Ich habe eine originalgetreue Nachbildung dieser Schleife über zwölf Eingabeformen instrumentiert, sowohl als Speicherstrom als auch als Dateistrom, und die Catch-Klausel griff in allen 24 Läufen null Mal. GZipStream verkettet Elemente selbst, zwei Elemente kommen also als ein Lesevorgang von 180 Byte an und die Schleife zählt ein einziges Element. Es duldet anhängenden Unrat selbst, eine gefüllte Datei wirft also ebenfalls nie. Bei jeder Form, die unsere Testsuite festschreibt, läuft die Schleife genau eine Runde und die Prüfung des Nachspanns trifft alle drei Entscheidungen: die gefüllte Datei annehmen, die abgeschnittene ablehnen, die Datei ablehnen, deren zweites Element abgeschnitten war. Der Behandler ist ein Überbleibsel, und sein Kommentar ist der einzige Ort, an dem die Füllregel aufgeschrieben ist.

Das zählt, weil die echte Regel enger ist als der Kommentar. Die Fülltoleranz kommt aus dem Durchlauf über 512 Byte, sie hält also genau so lange, wie der echte Nachspann innerhalb des Fensters bleibt. Ich habe Fülllängen von 0 bis 520 Byte durchgefahren: 504 Byte Füllung werden angenommen und 505 als abgeschnitten abgelehnt. Danach fällt die Prüfung auf die letzten vier Byte der Füllung durch und liest sie als Länge. Mit Nullen gefüllte Füllung übersteht die Grenze nur zufällig, denn vier Nullbytes lesen sich als Länge von nichts und die Regel, Kleineres anzunehmen, winkt sie durch. Füllung aus 0xFF derselben Länge liest sich als 4.294.967.295 und wird abgelehnt.

Ich habe nie für das Verhalten gemessen, das dieser Kommentar beschreibt. Dass Gegenstellen nach dem Nachspann Unrat anhängen, ist eine Behauptung in unserem Quelltext und ein Testname, keine Beobachtung.

Zwei Obergrenzen wohnen neben der Nachspannprüfung

Dieselbe Kopierschleife setzt zwei Grenzen durch, die mit dem Abschneiden nichts zu tun haben: eine absolute Grenze für geschriebene Byte und eine Verhältnisgrenze von entpackt gegen komprimiert, die eine kleine Datei fängt, die sich zu etwas ausdehnt, das kein Programmführer ist. Beide werden innerhalb der Schleife bei jedem Block von 80 KB geprüft, eine Bombe scheitert also auf halbem Weg statt nach dem vollständigen Schreiben.

Ich veröffentliche unsere aktuellen Zahlen nicht, und unsere eigenen Dokumente sind der Grund. Die Spezifikation des Dienstes, datiert auf den 19. Juli 2026, hält weiterhin ein Paar Abrufgrenzen fest, die die ausgelieferte Konfiguration seither überschritten hat. Bewegt hat sie ein echter nationaler Programmführer, der sich als komprimiert etwa 200 MB groß herausstellte und sich auf rund zwei Gigabyte ausdehnte: die komprimierte Grenze jener Zeit lag genau auf dieser Linie, und die entpackte Grenze lehnte die Datei rundheraus ab. Eine Zahl in einem Artikel altert genauso wie eine Zahl in einer Spezifikation. Also: die absolute Größe begrenzen, das Verhältnis begrenzen, beides während des Schreibens prüfen, und die absolute Grenze unter dem Punkt halten, ab dem ISIZE nicht mehr vergleichbar ist, sonst kostet Sie die Luft die Abschnittprüfung.

Zwei Einzelheiten tragen mehr als die Werte. Die Verhältnisgrenze gilt nur, wenn der Aufrufer die komprimierte Größe kennt, ein Aufrufer, der null übergibt, bekommt also allein die absolute Grenze, und unsere Abschnitttests tun genau das mit Absicht. Und der haushaltsweite Programmweg auf dem Gerät ruft den gemeinsamen Entpacker überhaupt nie auf: er hüllt einen rohen gzip-Strom in einen zählenden Strom, der eine Byte-Grenze durchsetzt und keine Verhältnisgrenze, und ruft dann die Nachspannprüfung unmittelbar auf. Das ist eine Lücke und keine Entscheidung, und ich konnte sie nirgends aufgeschrieben finden.

Die Sperre, die aus einem schlechten Rumpf einen dauerhaften macht

Jetzt die Hälfte, über die niemand schreibt.

Die Aufnahme des Programmführers ist teuer und die meisten Abrufe ändern nichts, der Abruf ist also bedingt: ein If-None-Match und ein If-Modified-Since aus dem, was der letzte Lauf gespeichert hat, und ein 304 beendet den Lauf. Prüfmerkmale allein genügen nicht. Ein öffentliches Verzeichnis, das wir auflösen, liefert ein ETag ohne Anführungszeichen aus, das nicht zurückläuft, bedingte Anfragen dagegen kürzen also nie ab. Die maßgebliche Sperre ist ein SHA-256 der entpackten Bytes, verglichen mit dem Hash, der auf der Zeile der Quelle gespeichert ist.

Ein Inhalts-Hash ist in jeder Hinsicht, auf die es ankommt, die bessere Sperre. Er wird lokal berechnet, und keine Gegenstelle kann ihn täuschen, indem sie über ihre Prüfmerkmale lügt oder sie vergisst. Er hat auch keinen Notausgang, und genau das ist das ganze Problem. Der Hash wird beim Abruf genommen, über die entpackte temporäre Datei, bevor ein Parser ein einziges Element angesehen hat, die Sperre kann also einen Rumpf, der sauber zusammengeführt wurde, nicht von einem unterscheiden, der zwei Schritte später explodiert ist.

Drei Schritte, und das Scheitern ist dauerhaft:

  1. Ein Rumpf kommt an, der nicht angenommen werden sollte, abgeschnitten oder schlicht unbrauchbar.
  2. Sein Hash wird berechnet und als Inhalts-Hash der Quelle gespeichert.
  3. Jeder spätere Abruf derselben Bytes ergibt denselben Wert und liefert "übersprungen, unverändert" zurück, ohne dass der Rumpf je wieder einen Parser erreicht.

Die Bedingung für die Erholung ist, dass sich die Bytes der Gegenstelle zufällig bewegen, und das ist die Formulierung, die der Kommentar des Entpackers selbst benutzt. Für einen nächtlich neu erzeugten nationalen Programmführer ist das ein Tag. Für eine Datei, die niemand neu erzeugt, ist es nie, und die Wiederholungsmechanik, die es für vorübergehende Fehler gibt, kann nicht helfen, denn ab dem zweiten Lauf gibt es keinen Fehler zu wiederholen.

Die letzte Drehung der Schraube: dieser Zustand meldet Gesundheit. Ein als unverändert übersprungener Lauf bedeutet, dass die Gegenstelle erreichbar und der Programmführer aktuell ist, der Gesundheitsauftrag zählt ihn also als bestanden und trägt die vorherige Senderzahl als Bestätigung weiter. Eine Quelle, die auf dem Hash eines nie zusammengeführten Rumpfes eingefroren ist, sieht auf jedem Dashboard, das wir haben, aus wie eine Quelle, der es gut geht.

Wir hatten den Schreibvorgang an der falschen Stelle

Unsere eigene Fassung davon hatte mit dem Abschneiden nichts zu tun. Es war die Reihenfolge.

Die Prüfmerkmale wurden früher unmittelbar nach dem Abruf geschrieben, vor der Auswertung und vor der Zusammenführung. Ein Lauf, der einen guten Rumpf abrief und dann später scheiterte, an einem Aussetzer der Datenbank, an einem Abbruch wegen Speichermangels, an einem Wartungsauftrag, der damals jede Zwischentabelle eines Laufs löschte, darunter eine, die eine noch laufende Aufnahme gerade benutzte, hielt den Lauf als gescheitert fest und speicherte den neuen Hash trotzdem. Der nächste Zyklus rief dieselben Bytes ab, fand eine Übereinstimmung und übersprang. Die Quelle veraltete und meldete Gesundheit, und kein noch so häufiges Wiederholen rührte sie an.

Der Zusammenstoß mit der Zwischentabelle ist inzwischen für sich behoben: die Wartung überspringt jede Tabelle, deren Lauf noch als laufend markiert und jünger als die Verwaisungsschwelle ist. Die Korrektur der Reihenfolge ist ein verschobener Aufruf, und der Kommentar dort, wo der Schreibvorgang jetzt landet, sagt warum:

// Validators persist only after the merge has succeeded. Writing
// them right after the fetch made any parse or merge failure
// permanent: the run was marked failed, but the new hash was already
// stored, so every later fetch of the same bytes short-circuited to
// skipped_unchanged and the source never re-merged until upstream
// changed. With the write here, a failed run leaves the old
// validators in place and the next cycle retries the full ingest.

Der 304-Weg behält seinen eigenen Schreibvorgang, und der ist sicher, denn ein 304 belegt, dass der Inhalt dem entspricht, was bereits zusammengeführt ist, und die Prüfmerkmale aufzufrischen kann nichts verdecken. Die Regel lautet, Prüfmerkmale erst dann zu schreiben, wenn der Rumpf für Sie fertig nützlich gewesen ist.

Der Regressionstest hat die Form, die ich mir für jede solche Sperre wünschen würde. Er stellt eine echte Gegenstelle auf, die 22 Zeichen kaputtes XML ausliefert, prüft, dass der Lauf gescheitert ist und der gespeicherte Inhalts-Hash weiterhin leer, und lässt ihn dann über dieselben Bytes erneut laufen und prüft, dass er ein zweites Mal scheitert, statt zu überspringen. Ein Spiegeltest belegt, dass die Übersprungoptimierung nach einem Erfolg weiterhin wirkt, die Korrektur kann also nicht versehentlich "hör auf zu zwischenspeichern" lauten.

Wir haben das Original gefunden, indem wir bei einer Plattformprüfung im August 2026 unseren eigenen Code gelesen haben, und nicht aus einer Meldung, und das ist der unangenehme Teil. Die Prüfung erzeugte 110 Befunde und stufte diesen als hoch ein. Ich kann Ihnen nicht sagen, wie oft er im Betrieb ausgelöst hat, denn der Zustand, den er erzeugt, ist von Gesundheit nicht zu unterscheiden, und der Programmführer, den er ausliefert, ist alt statt abwesend.

Das Gerät hat dieselbe Form und keinen Hash

Die Serverhälfte ist behoben. Die Gerätehälfte nicht, und ich schreibe das lieber auf, als diese Seite Symmetrie andeuten zu lassen.

Der Programmimport je Quelle unserer Windows-App speichert sein ETag und sein Last-Modified eine Zeile nach der Rückkehr des Abrufs, vor dem Entpacken, vor der Auswertung, bevor irgendetwas den lokalen Speicher erreicht. Kein Codepfad löscht sie danach. Die Fehlersenke löscht die halb geschriebene Generation des Programmführers und lässt die Abruf-Metadaten in Ruhe; das Aufräumen rührt keines von beidem an. Eine Gegenstelle, die bedingte Anfragen beachtet, kann uns also einen Rumpf ausliefern, den wir nicht auswerten können, unsere aufgefrischten Prüfmerkmale entgegennehmen und die nächste Anfrage mit einem 304 beantworten, das nichts zusammenführt.

Zwei Dinge mildern das, und keines schließt es. Das Gerät führt überhaupt keinen Inhalts-Hash, nur die beiden Prüfmerkmale, die Falle braucht also eine Gegenstelle, die bedingte Anfragen tatsächlich beachtet, und die mit kaputten Prüfmerkmalen sind zufällig immun. Und ein gescheiterter Import lässt den vorherigen Programmführer lesbar, denn der Zeiger auf die Generation wird erst beim Abschluss umgeschaltet, das sichtbare Symptom ist also ein Programmführer, der stehen bleibt, und keiner, der verschwindet. Das ist ein besseres Scheitern als das des Servers, und es ist trotzdem derselbe Fehler.

Der haushaltsweite Weg, der die Programmführer eines ganzen Haushalts in einen einzigen Speicher zieht, macht das auf eine interessante Weise richtig. Er kann nicht in einen Puffer entpacken und dann prüfen, denn die Gesamtmenge ist zu groß, um sie zu halten, er wertet also unmittelbar aus dem gzip-Strom aus und prüft den Nachspann danach. Vor dem Prüfen leert er, was der XML-Leser übrig gelassen hat, denn die an die Prüfung übergebene Anzahl muss das ganze Element sein und nicht das, wie weit der Leser zufällig gekommen ist. Erst auswerten, leeren, dann prüfen. Diese Reihenfolge ist lästig zu lesen und sie ist die einzige, die funktioniert.

Was Ihnen das bringt

Unsere Windows-App führt diesen Entpacker auf Ihrem Rechner aus, gegen die Programmadresse, die Sie ihr gegeben haben, und zu keinem Zeitpunkt läuft ein Strom durch uns. Ein Programmführer, der die Nachspannprüfung nicht besteht, lässt den, den Sie bereits haben, auf dem Bildschirm, und das ist das Verhalten, das Sie von einer Hintergrundaktualisierung wollen, bei der Sie nicht zugesehen haben. Sie können ein kostenloses Konto anlegen und es auf eine Quelle richten, die Sie bereits haben.

Wenn Sie von dieser Seite eine Gewohnheit mitnehmen, dann die, dem Wort "unverändert" zu misstrauen. Eine Zwischenspeicher-Sperre, die auf den Inhalt eines Rumpfes abstellt, ist die richtige Sperre und die, die ich wieder bauen würde, und sie ist zugleich eine Sperre ohne jede Möglichkeit zu sagen "ich habe diesen Rumpf angenommen und danach ging alles schief". Was Sie auch immer an der Tür berechnen, speichern Sie es an der Stelle, an der Sie wissen, dass die Arbeit gelungen ist, und geben Sie Ihrer Gesundheitsprüfung etwas zu betrachten, das nicht das Fehlen eines Fehlers ist.

Was dieser Artikel gemessen hat32 Behauptungen, jeweils mit den Beweisen dahinter
AnspruchBeweiseGezählt
Eine gzip-Datei ist eine Folge von Elementen; jedes Element besteht aus einem festen Kopf von zehn Byte mit optionalen Feldern danach, einem deflate-Strom, und einem Nachspann von acht Byte, der CRC32 und danach ISIZE trägt, die unkomprimierte Größe modulo 2^32.RFC 1952 section 2.2 (file format, series of members), section 2.3 (member format), section 2.3.1 (member header and trailer, which defines both CRC32 and ISIZE as the size of the original input modulo 2^32). https://www.rfc-editor.org/rfc/rfc1952.htmlSpezifikationNicht zutreffend
Jeder deflate-Block trägt ein BFINAL-Bit, das markiert, ob er der letzte Block des Datensatzes ist, ein Entpacker kann einen Strom also beenden, ohne je zu lesen, was danach kommt.RFC 1951 section 3.2.3: "BFINAL is set if and only if this is the last block of the data set." https://www.rfc-editor.org/rfc/rfc1951.htmlSpezifikationNicht zutreffend
Eine Antwort ohne angegebene Länge, deren Rumpf endet, wenn die Verbindung geschlossen wird, gilt als vollständig, sofern die darunterliegende Verbindung keinen Fehler gemeldet hat, der Client hat also nichts, womit er die empfangene Länge vergleichen könnte.RFC 9112 section 6.3 rule 8 (body length is the octets received before the server closes the connection) and section 8, Handling Incomplete Messages. https://www.rfc-editor.org/rfc/rfc9112.htmlSpezifikationNicht zutreffend
Mit dem gesamten Nachspann von acht Byte entfernt gibt GZipStream die vollständige Nutzlast zurück und wirft nichts.n = 123.08.2026
Keine Schnitttiefe ist am Entpacker erkennbar: 1 bis 50 Byte von einer Datei mit einem Element zu entfernen liefert bei jeder Tiefe eine Teilausgabe und wirft nichts, 90 Byte zurück bei einem Schnitt von 1 Byte und 48 Byte zurück bei einem Schnitt von 40 Byte.n = 5023.08.2026
Ein Nachspann, der vorhanden, aber beschädigt ist, wirft sehr wohl: ein einzelnes gekipptes Bit an einer der acht Byte-Positionen des Nachspanns löst eine InvalidDataException aus, ohne dass etwas geliefert wird.n = 823.08.2026
Fünf Abschnittlängen sind als Fehler der Kette festgeschrieben: 1, 4, 8, 20 und 40 Byte, entfernt von einem Element über einem Dokument von 90 Byte, und alle fünf Ablehnungen kommen von der Prüfung des Nachspanns und nicht von GZipStream.n = 523.08.2026
Die Prüfung des Nachspanns liest ein Endfenster von 512 Byte, überspringt Dateien unter 18 Byte und nicht positionierbare Ströme, durchsucht jeden Versatz nach einem möglichen Nachspann, dessen zweites Wort der Anzahl entpackter Byte entspricht, und liest nur dann, wenn keiner passt, die letzten vier Byte und scheitert, wenn sie größer sind als das Entpackte.n = 123.08.2026
Die Untergrenze von 18 Byte ist ein Kopf plus ein Nachspann mit nichts dazwischen; das kleinste echte gzip-Element ist größer, nämlich 21 Byte für ein Byte Eingabe.n = 123.08.2026
505 mögliche Versätze gegen einen Wert von vier Byte ergeben eine Falschübereinstimmung von rund eins zu achteinhalb Millionen, wenn die Endbytes nichts mit der entpackten Anzahl zu tun haben.Arithmetic: the loop condition i + 8 <= 512 runs i in [0, 504], so 505 offsets; 505 * 2^-32 = 1.18e-7, or 1 in 8.50 million. Offset count confirmed by measurement.SpezifikationNicht zutreffend
Wenn der Nachspann fehlt, sind die letzten vier Byte komprimierte Daten, die als Länge gelesen werden, und die Prüfung nimmt immer dann an, wenn dieser Wert nicht größer ist als die entpackte Anzahl: rund eins zu hundertsieben bei 40 MB entpackt, und 0.47 bei zwei Gigabyte entpackt.Arithmetic: P(accept) = written / 2^32; 40e6/2^32 = 0.0093 (1 in 107.4), 2e9/2^32 = 0.466.SpezifikationNicht zutreffend
Anhängende Füllbytes nach einem sauber beendeten Element werden nur so lange geduldet, wie der echte Nachspann innerhalb des Fensters von 512 Byte bleibt: 504 Byte Füllung werden angenommen und 505 als abgeschnitten abgelehnt.n = 52123.08.2026
GZipStream verkettet Elemente und duldet anhängenden Unrat von sich aus, bei jeder Form, die die Testsuite festschreibt, läuft die Elementschleife also eine Runde, ihre Catch-Klausel greift null Mal, und jedes Urteil kommt von der Prüfung des Nachspanns.n = 1223.08.2026
Die Prüfung steigt aus, wenn die entpackte Anzahl uint.MaxValue übersteigt, denn ISIZE ist die Größe modulo 2^32, ein Lauf, dessen Ausgabe vier Gibibyte erreicht, verliert die Prüfung also stillschweigend.n = 123.08.2026
Alle drei Aufnahmewege entpacken aus einer Datei oder einem Puffer, die positionsabhängige Prüfung läuft also auf allen dreien.n = 323.08.2026
Die Kopierschleife setzt eine absolute Byte-Grenze und eine Verhältnisgrenze von komprimiert zu entpackt durch, beide bei jedem Block von 80 KB geprüft; die Verhältnisgrenze gilt nur, wenn der Aufrufer eine komprimierte Größe mitliefert.n = 123.08.2026
Die beiden Entpacker sind dieselbe Datei, zwischen den Codebasen portiert, wobei die absolute Standardobergrenze im Verhältnis acht zu eins auseinandergeht.n = 223.08.2026
Der haushaltsweite Weg auf dem Gerät hüllt seinen Entpacker in einen zählenden Strom, der eine Byte-Grenze durchsetzt und überhaupt keine Verhältnisgrenze.n = 123.08.2026
Die Spezifikation des Dienstes hält weiterhin ein Paar Abrufgrenzen fest, die die ausgelieferte Konfiguration bereits überschritten hat.n = 123.08.2026
Ein echter nationaler Programmführer läuft komprimiert auf etwa 200 MB hinaus und dehnt sich auf rund zwei Gigabyte aus; die komprimierte Grenze jener Zeit lag genau auf dieser Linie und die entpackte Grenze lehnte die Datei rundheraus ab.n = 123.08.2026
Die Aufnahme ruft bedingt ab und hängt an einem SHA-256 der entpackten Bytes, weil mindestens eine Gegenstelle ein ETag ausliefert, das nicht zurückläuft.n = 123.08.2026
Der Hash wird beim Abruf berechnet, bevor ein Parser die Bytes sieht, ein Auswertungsfehler und ein Zusammenführungsfehler sind für die Sperre also nicht unterscheidbar.n = 123.08.2026
Ein Rumpf, der kürzer ist als eine angegebene Content-Length, löst eine HttpRequestException aus, die eine HttpIOException umschließt, "The response ended prematurely"; dieselben 40 Byte, per Verbindungsschluss begrenzt geliefert, kommen ganz ohne Fehler zurück.n = 223.08.2026
Die Prüfmerkmale wurden früher unmittelbar nach dem Abruf gespeichert, was jeden Auswertungs- oder Zusammenführungsfehler dauerhaft machte; sie werden jetzt erst nach einer erfolgreichen Zusammenführung gespeichert, und der 304-Weg behält seinen eigenen Schreibvorgang.n = 123.08.2026
Die Prüfung, die es gefunden hat, erzeugte 110 Befunde und stufte diesen als hoch ein.n = 11011.08.2026
Ein geplanter Wartungsauftrag löschte früher jede Zwischentabelle eines Laufs, darunter eine, die eine noch laufende Aufnahme gerade benutzte, und das war der konkrete Verursacher eines Fehlschlags mitten im Lauf; er schließt jetzt Tabellen aus, deren Lauf noch läuft und die jünger sind als die Verwaisungsschwelle.n = 123.08.2026
Ein als unverändert übersprungener Lauf zählt als bestandene Gesundheitsprüfung und trägt die zuvor bestätigte Senderzahl weiter.n = 123.08.2026
Der Regressionstest liefert 22 Zeichen kaputtes XML aus, prüft, dass der Lauf scheitert und der Inhalts-Hash weiterhin leer ist, und prüft, dass ein zweiter Lauf über dieselben Bytes erneut scheitert, statt zu überspringen; ein Spiegeltest belegt, dass die Übersprungoptimierung nach einem Erfolg weiterhin wirkt.n = 223.08.2026
Der Weg je Quelle auf dem Gerät speichert sein ETag und sein Last-Modified eine Zeile nach der Rückkehr des Abrufs, vor dem Entpacken und der Auswertung, und kein Codepfad löscht sie, auf einen Auswertungsfehler kann also ein 304 folgen, das nichts zusammenführt.n = 123.08.2026
Der Weg je Quelle auf dem Gerät behält nur ein ETag und ein Last-Modified, ohne Inhalts-Hash, seine Fassung der Falle hängt also davon ab, dass die Gegenstelle bedingte Anfragen beachtet.n = 123.08.2026
Ein gescheiterter Import lässt den vorherigen Programmführer lesbar, denn der Zeiger auf die Generation wird erst beim Abschluss umgeschaltet.n = 123.08.2026
Der haushaltsweite Weg wertet zuerst aus und prüft den Nachspann danach, wobei er leert, was der XML-Leser übrig gelassen hat, damit die an die Prüfung übergebene Anzahl das ganze Element ist.n = 123.08.2026