Wie man unabsichtlich JPEGs vernudelt

Ups! Ein 13 Jahre alter Bug, den ich seither immer mitschleppe.

Tja, aber nachdem mich bisher niemand angerufen und “nach Support” gefragt hat, wird der wohl bei meinen früheren “Kunden” nie aufgetreten sein … oder?

Es geht um die liebe libJPEG und ihren Callback zum Überspringen von Datenblöcken.


Ich glaube Anfang 2008 habe ich erstmals mit der libJpeg gearbeitet und mir damals eine Experimentierumgebung zu Hause eingerichtet, von der ich das Ergebnis immer in Projekte wegkopiert habe.

Dabei nutze ich nicht die Standard-Funktionen, die nur auf Dateibasis arbeiten, sondern registriere Callbacks für das Lesen und Schreiben von Daten, in denen ich auch eigene Streams umleiten kann.

Und da ist mir offenbar eine Funktion unter den Tisch gefallen.
Sie heißt skip_input_data und wird vom JPEG-Decoder aufgerufen um uninteressante Datenblöcke zu überspringen.
Besteht ein JPEG nur aus Bilddaten, kommt diese Funktion kaum zum Zug, doch heutige Digitalkameras schreiben hunderte Kilobytes an Zusatzinfos in den Datenstrom. Geokoordinaten, Thumbnails und anderes Gewäsch wird mit unterschiedlichsten Zusatzheadern eingebaut.

libJPEG fängt mit diesen Daten nichts an und überspringt sie einfach um zu den richtigen Bilddaten zu kommen. Und genau dann wird skip_input_data aufgerufen.

Und ich dachte bisher, dass diese Funktion nur eine optionale Optimierung sei und habe sie leer implementiert.
Tja, das war aber so was von falsch!

Die Standard-Implementierung sieht nämlich so aus:

 1static void skip_input_data(j_decompress_ptr cinfo, long num_bytes)
 2{
 3  struct jpeg_source_mgr * src = cinfo->src;
 4  size_t nbytes;
 5  if (num_bytes > 0) 
 6  {
 7    nbytes = (size_t) num_bytes;
 8    while (nbytes > src->bytes_in_buffer) 
 9        {
10      nbytes -= src->bytes_in_buffer;
11      (void) (*src->fill_input_buffer) (cinfo);
12    }
13    src->next_input_byte += nbytes;
14    src->bytes_in_buffer -= nbytes;
15  }
16}

Es werden so lange Daten nachgeladen, bis die zu überspringende Menge im zuletzt geladenen Block angekommen ist. Dort wird dann nur noch der Pointer weiter gerückt die Anzahl der verfügbaren Bytes reduziert.

Ohne diesen Codebatzen ruckelt der Decoder offenbar selbst weiter. Manchmal klappt das, oft aber nicht.
Hätte ich nicht ein grafisches Demoprojekt im GATE Framework gestartet, das Bilder laden und anzeigen kann, wäre mir das auch weiter nicht aufgefallen.
Denn das Tool konnte kein einziges Bild meiner Smartphone-Kamera lesen.

Fazit

Nun … echt peinlich so etwas! Denn diesen Bug habe ich in mehrere kommerzielle Produkte in den letzten 12 Jahren hineinkopiert.
Doch zum Glück war das Lesen von JPEGs in keinem einzigen betriebsrelevant. Es wurden immer nur JPEGs erzeugt und diese eventuell später neu geladen um noch was darüber zu zeichnen.

Da kamen also nie Metadaten vor, die den Sprungmarken-Bug auslösen konnten.

Hmm … ich frage mich, wie viele andere unentdeckte Bugs ich sonst noch wo verbaut habe… und … wie viele andere Programmierkollegen ähnliche Verschreibser in Ihren Lebensläufen haben.

Eines steht für mich aber fest: Mit dem GATE Projekt komme ich endlich dazu solche Langzeit-Bugs aus meinen Codebases und meinem Hirn zu vertreiben. Eine solche Reinigungskur kann ich daher nur weiterempfehlen.