January 28, 2021·5 min read·engineeringcglibcsecurityencoding

The ISO-2022-JP-3 conversion contract behind CVE-2021-3326

A glibc converter could report a full internal buffer while leaving space unused. The resulting assertion failure shows why partial conversion state belongs in the security analysis of text processing.

A character converter can run out of room without having filled its output buffer. That sounds harmless until its caller relies on those two conditions being equivalent. The glibc ISO-2022-JP-3 converter has exactly this problem when a source character expands into two wide characters. Its failure to preserve partial output violates an internal conversion contract and reaches an assertion in the shared conversion machinery.

The January 28 oss-security message assigns CVE-2021-3326 to the crash. The preceding discussion describes a mail client crashing when it encounters an affected subject. That is a useful account of the exposure because a subject is input a reader may process before deciding whether to open a message. The public evidence here establishes a crash. It does not establish arbitrary code execution, and there is no reason to inflate the report into that claim.

C was my first language, and its explicit handling of memory remains part of how I understand systems. This failure interests me because neither an obviously invalid pointer nor an unchecked copy explains it. The converter and its caller disagree about the meaning of a return status. Understanding the allocated buffer is necessary, but understanding the protocol between the functions is necessary too.

Expansion changes the stopping condition

Florian Weimer's January 27 patch identifies the relevant contract. The conversion loop may return __GCONV_FULL_OUTPUT only after completely filling the internal wide-character buffer. An assertion in the common code checks that condition. The ISO-2022-JP-3 loop instead stops when it lacks room for the entire two-character result of one source character.

Consider the intermediate result as a sequence of wide-character slots. One slot remains, but the next source character needs two. A converter that treats that expansion as indivisible cannot write it. Reporting a full buffer then seems locally reasonable because the next operation cannot fit. The caller, however, expects the remaining slot to contain output. The error follows from a stronger definition of full in the common interface than the module implements.

This explanation also distinguishes the internal buffer from the application's destination buffer. The patch describes conversion to an internal encoding. Its regression test requests UTF-8 output. A reader looking only at the application's byte count could miss the intermediate wide-character boundary that triggers the assertion. Several conversion stages can each have a legitimate notion of remaining capacity, and their status values have to preserve the distinctions.

The old implementation checks whether eight bytes remain before writing both wide characters. When that check fails, it reports the full-output status without writing either one. The patch changes the sequence. It consumes the source character, writes the first wide character, then checks whether another four bytes remain for the second. If the second character cannot fit, the converter stores it in the conversion state for a subsequent iteration.

That storage makes the partial operation representable. Input has advanced even though the full corresponding output has not yet reached its destination. A correct continuation therefore needs more information than the current input pointer. It needs the pending character as well. The amended state comment documents this use for combining sequences in the affected character sets.

A pending character survives the call

The patch gives pending output priority over fresh input. At the start of the conversion loop, it checks for a stored wide character. When one exists, it writes that character and removes it from the pending state while retaining the encoding's state bits. Only after that work does the converter proceed to another source byte. This order preserves the original output sequence across a buffer boundary.

Flushing needs the same treatment. The previous code could reset the input encoding state without emitting anything. Once a character can remain queued, resetting alone would discard output that belongs to input already consumed. The new flush handling first makes room for the pending wide character and writes it before returning to the initial state. If there is insufficient space, it reports the full-output condition instead.

The change therefore affects normal conversion and finalization together. Fixing only the main loop would leave a second route to incorrect behavior when the caller finishes the stream. This is an engineering consequence of the chosen representation. Moving an incomplete operation into persistent state creates an obligation for every operation that clears or resets that state.

I regard that obligation as the most useful part of the patch to study. C makes a state representation cheap and explicit, but it does not make the representation's lifecycle self-enforcing. A field that previously held only encoding state now also carries deferred output. Every path that interprets or discards it has to account for the added meaning.

The regression test checks progress

The new bug-iconv14.c exercises conversion with a three-byte destination buffer. Its first call produces two bytes and reports E2BIG. A subsequent call produces the remaining two bytes. Those observations matter together. The initial call has made progress, and the continuation must neither repeat the first output nor lose the second part.

The test covers two ways to finish the pending work. One input includes an escape sequence returning to the initial encoding state. The other ends after the character expansion and uses an explicit flush. Both paths check the output and pointer accounting. A second flush must produce no further output, which verifies that the pending character was cleared after emission.

This is a stronger regression case than a check that a process no longer crashes. Removing an assertion could satisfy such a check while leaving incorrect text or a broken continuation. The supplied test constrains the conversion's observable progress, including the amount of input consumed. That matters because a streaming caller uses those updated pointers to decide what to submit next.

The security significance follows from the same state machine. If an application processes externally supplied text through the affected conversion, the input can reach code whose assumptions fail inside the library. The mail-subject example makes the availability consequence concrete. Assessing a particular application still requires establishing that it uses this conversion path, rather than assuming every text operation reaches it.

The disclosed repair retains support for the encoding and represents the two-character expansion across calls. It also preserves the common conversion machinery's definition of a full internal buffer. For callers, the intended result is ordinary partial-conversion behavior, with deferred output delivered on continuation or flush and with the input position reflecting the bytes already consumed.