Re: [PATCH 1/2] iconvdata: SHIFT_JISX0213 lacks pending character reset (CVE-2026-77117)

Carlos O'Donell <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Organization Red Hat, LLC.
Message-ID <[email protected]>
On 8/21/26 6:12 AM, Florian Weimer wrote:
> This fixes bug 34556.

LLM-assisted review found what you already noted in the ticket that
euc-jisx0213.c has the same flaw, but I'll ignore this for now since
that flaw will get it's own bug and a new reserved CVE ID.

Reviewed-by: Carlos O'Donell <[email protected]>

> ---
>   iconvdata/shift_jisx0213.c | 3 +++
>   1 file changed, 3 insertions(+)
> 
> diff --git a/iconvdata/shift_jisx0213.c b/iconvdata/shift_jisx0213.c
> index 61c9c3ce6d..e9179f605e 100644
> --- a/iconvdata/shift_jisx0213.c
> +++ b/iconvdata/shift_jisx0213.c
> @@ -226,6 +226,9 @@
>   	    STANDARD_FROM_LOOP_ERR_HANDLER (1);				      \
>   	  }								      \
>         }									      \
> +    else 								      \
> +      /* There was a pending character.  Clear it.  */			      \
> +      *statep = 0;							      \

OK. We load the pending into ch, and then clear it, and then output it.

This is required because:

(a) EMIT_SHIFT_TO_INIT is only triggered when flushing the stream, and
     it would emit the pending character, but it is not required to flush
     between calls.

(b) BODY must be able to handle a restart according to the POSIX wording:
     "Subsequent calls with inbuf as other than a null pointer or a pointer
      to a null pointer cause the conversion to take place from the current
      state of the conversion descriptor."

The CVE fix looks good to me.

There is another bug present here that results in data loss and should
probably get it's own bug filed...

What happens if the combining character is the last character?

In such a case loop.c skips calling BODY (because there is no input to
consume) in that case and returns a success but drops the combining
character even if there is room, and this leads to a data loss scenario?

LLM-assisted test case:

   FAIL: iconvdata/tst-shift-jisx0213-e2big-loss
   info: with flush:    produced 2 code point(s): U+304B U+309A
   info: without flush: produced 1 code point(s): U+304B
   tst-shift-jisx0213-e2big-loss.c:165: error: blob comparison failed
     left  (no_flush):  4B 30 00 00                        ← only U+304B
     right (expected):  4B 30 00 00 9A 30 00 00            ← U+304B U+309A

Shall we consider this another distinct bug since data is lost?

~~~
/* Test SHIFT_JISX0213 combining-character loss across an E2BIG boundary.
    Copyright (C) 2026 Free Software Foundation, Inc.
    This file is part of the GNU C Library.

    The GNU C Library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    The GNU C Library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */

/* SHIFT_JISX0213 bytes 0x82 0xF5 map to a combining sequence of two UCS-4
    code points: U+304B (HIRAGANA LETTER KA) + U+309A (COMBINING SEMI-VOICED
    SOUND MARK).  When the output buffer has room for only the first code
    point, the converter emits U+304B, consumes the whole input character, and
    queues U+309A in the converter state, returning E2BIG.

    This exercises the case where the combining character is the *last* input
    unit and the application continues per the POSIX E2BIG protocol - drain the
    output already produced, supply a larger output buffer, and call iconv()
    again with the same descriptor - WITHOUT issuing a terminating
    iconv (cd, NULL, ...) flush.  POSIX does not require such a flush to
    continue or complete a conversion after E2BIG.

    Because all input was consumed by the first call, the continuation call
    never re-enters the conversion loop body, so the queued U+309A is never
    emitted: iconv () reports success with all input consumed, but the second
    code point is silently dropped.  It surfaces only if the application
    happens to issue a flush.

    A correct conversion of the complete input must yield both code points
    regardless of where the output-buffer boundary falls, so the check at the
    end of do_test asserts that both are produced without a flush.  Against a
    buggy library this comparison FAILS, demonstrating the data loss; the
    accompanying with-flush pass confirms the byte was merely parked in the
    converter state rather than corrupted.  */

#include <errno.h>
#include <iconv.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>

#include <support/check.h>
#include <support/support.h>

/* The single combining character, and nothing after it.  */
static const char sjis_combining[] = { '\x82', '\xf5' };

/* Its complete expansion in WCHAR_T (UTF-32, host endianness).  */
static const wchar_t expected[] = { 0x304b, 0x309a };

/* Convert the input described above using an output window of WINDOW bytes,
    following
    the POSIX E2BIG-continuation pattern.  If FLUSH is nonzero, a terminating
    iconv (cd, NULL, ...) flush is issued after all input is consumed.  The
    produced bytes are appended to RESULT (of RESULT_SIZE bytes); the total
    number of bytes produced is returned.  */
static size_t
convert (size_t window, char *result, size_t result_size, int flush)
{
   iconv_t cd = iconv_open ("WCHAR_T", "SHIFT_JISX0213");
   TEST_VERIFY_EXIT (cd != (iconv_t) -1);

   char window_buf[8];
   TEST_VERIFY_EXIT (window <= sizeof window_buf);

   char *inptr = (char *) sjis_combining;
   size_t inleft = sizeof sjis_combining;
   size_t result_len = 0;
   int guard = 0;

   /* Ordinary application loop: feed input, and on E2BIG drain the produced
      output and retry with a fresh output window.  Stop once all input has
      been consumed.  No flush happens here.  */
   while (inleft > 0)
     {
       char *outptr = window_buf;
       size_t outleft = window;

       errno = 0;
       size_t r = iconv (cd, &inptr, &inleft, &outptr, &outleft);
       size_t produced = outptr - window_buf;

       TEST_VERIFY_EXIT (result_len + produced <= result_size);
       memcpy (result + result_len, window_buf, produced);
       result_len += produced;

       if (r == (size_t) -1)
     {
       if (errno == E2BIG)
         {
           /* Detect a non-progress / infinite-loop failure mode.  */
           TEST_VERIFY_EXIT (++guard < 16);
           continue;
         }
       FAIL_EXIT1 ("window %zu: iconv: %m", window);
     }

       /* Success: the entire input has been consumed.  */
       break;
     }

   TEST_COMPARE (inleft, 0);

   if (flush)
     {
       char *outptr = window_buf;
       size_t outleft = window;

       errno = 0;
       size_t r = iconv (cd, NULL, NULL, &outptr, &outleft);
       TEST_VERIFY_EXIT (r != (size_t) -1);
       size_t produced = outptr - window_buf;

       TEST_VERIFY_EXIT (result_len + produced <= result_size);
       memcpy (result + result_len, window_buf, produced);
       result_len += produced;
     }

   TEST_VERIFY_EXIT (iconv_close (cd) == 0);
   return result_len;
}

static void
report (const char *what, const char *result, size_t len)
{
   size_t ncp = len / sizeof (wchar_t);
   printf ("info: %s: produced %zu code point(s):", what, ncp);
   for (size_t i = 0; i < ncp; i++)
     printf (" U+%04X", (unsigned int) ((const wchar_t *) result)[i]);
   putchar ('\n');
}

static int
do_test (void)
{
   /* An output window of exactly one UCS-4 code point (4 bytes) forces the
      boundary to fall between U+304B and U+309A.  */
   enum { WINDOW = sizeof (wchar_t) };

   /* With a terminating flush the queued code point is recovered: this shows
      the byte is parked in converter state, not corrupted.  */
   char with_flush[64];
   size_t with_flush_len = convert (WINDOW, with_flush, sizeof with_flush, 1);
   report ("with flush", with_flush, with_flush_len);
   TEST_COMPARE_BLOB (with_flush, with_flush_len, expected, sizeof expected);

   /* Without a flush - the POSIX continuation pattern - the second code point
      of the combining sequence is dropped.  */
   char no_flush[64];
   size_t no_flush_len = convert (WINDOW, no_flush, sizeof no_flush, 0);
   report ("without flush", no_flush, no_flush_len);

   /* The conversion of the complete input must produce both code points even
      though the application issued no flush.  This FAILS on a library that
      drops the queued U+309A, demonstrating the data loss.  */
   TEST_COMPARE_BLOB (no_flush, no_flush_len, expected, sizeof expected);

   return 0;
}

#include <support/test-driver.c>
~~~

>   									      \
>       put32 (outptr, ch);							      \
>       outptr += 4;							      \
> 
> base-commit: 6467136459cc86a4426e35ac8d8f9de4673b9f1f


-- 
Cheers,
Carlos.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.