Re: BUG #19771: to_char(): NUL byte in result for formats with a trailing decimal point (regression from ec352b37d)

From: Gowtham Reddy <gowthamdoma18(at)gmail(dot)com>
To: gowthamdoma18(at)gmail(dot)com, pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: tgl(at)sss(dot)pgh(dot)pa(dot)us
Subject: Re: BUG #19771: to_char(): NUL byte in result for formats with a trailing decimal point (regression from ec352b37d)
Date: 2026-10-18 20:47:07
Message-ID: CAOZ9b+osn=upOQvLYP9uTADw7CqP2nA6Uogsmv9dyQh4EB18ZQ@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

Hello,

Please find attached a proposed patch for BUG #NNNNN.

It stops NUM_numpart_to_char() from emitting the number string's
terminating '\0' as a digit, and adds a regression test to numeric.sql
(the test fails without the fix). make check passes (239/239) on
master 45fc51fcbf45.

Tom, CCing you since this became visible with ec352b37d.

Regards,
Gowtham

On Mon, 19 Oct 2026 at 02:11, PG Bug reporting form
<noreply(at)postgresql(dot)org> wrote:
>
> The following bug has been logged on the website:
>
> Bug reference: 19771
> Logged by: Gowtham Reddy
> Email address: gowthamdoma18(at)gmail(dot)com
> PostgreSQL version: 19rc1
> Operating system: Linux x86_64
> Description:
>
> Hello,
>
> On master (tested at 45fc51fcbf45), to_char() for numbers returns a text
> value containing a NUL byte when the format has a decimal point with no
> digit positions after it:
>
> SELECT encode(textsend(to_char(7, '9.')), 'hex');
> -- master: 203700 REL_19: 2037
> SELECT encode(textsend(to_char(7, '9.PL')), 'hex');
> -- master: 2037002b REL_19: 2037
> SELECT length(to_char(7, '9.')), octet_length(to_char(7, '9.'));
> -- master: 2, 3
> SELECT to_char(7, '9.') = ' 7';
> -- master: false REL_19: true
> SELECT encode(textsend(to_char('100'::numeric, 'FM999.')), 'hex');
> -- master: 31303000
>
> The NUL byte is also written to tables: CREATE TABLE t AS SELECT
> to_char(7, '9.PL') stores 2037002b. The existing regression query in
> numeric.sql (SELECT to_char('100'::numeric, 'FM999.')) returns '100\0' on
> master but still passes, because psql stops printing at the NUL.
>
> Bisected to:
>
> ec352b37d Replace formatting.c's fixed-size output buffers with
> StringInfos.
>
> Its parent 28c5d0724 returns 2037 for the second query.
>
> The underlying problem is older. In NUM_numpart_to_char(), the "Write
> Digits" branch copies *Np->input_p even when the number string is already
> exhausted, so it copies the terminating '\0'. With the old fixed-size
> buffer that NUL ended the C string, so in released branches everything
> after the trailing decimal point is silently dropped, including the sign:
>
> SELECT to_char(-7, '9.MI'); -- REL_19: '7' (sign lost)
> SELECT to_char(-7, '9.PR'); -- REL_19: '<7' (closing bracket lost)
> SELECT to_char(7, '9.PL'); -- REL_19: ' 7' (PL lost)
> SELECT to_char(7, '9."x"'); -- REL_19: ' 7' (literal lost)
>
> With the StringInfo output buffer the NUL is kept, and the suffix appears
> after it.
>
> Proposed patch (attached, v1-0001): do not emit the string terminator as a
> digit. It also adds a regression test to numeric.sql, which fails without
> the fix. The core change is:
>
> --- a/src/backend/utils/adt/formatting.c
> +++ b/src/backend/utils/adt/formatting.c
> @@ -5659,7 +5659,7 @@ NUM_numpart_to_char(NUMProc *Np, int id)
> NUM_EMITC('0');
> }
> }
> - else
> + else if (*Np->input_p != '\0')
> {
> NUM_EMITC(*Np->input_p); /*
> Write DIGIT */
> Np->num_in = true;
>
> With this, master returns ' 7', ' 7+', '7-', '<7>', ' 7x' for the queries
> above, ' 1,234' for to_char(1234, '9,999.'), and '100' for the numeric.sql
> case. Formats without a trailing decimal point are unchanged ('9.9',
> 'FM9.9', '99.99PL', '99PR'). make check passes (239/239) on master
> 45fc51fcbf45 with the patch.
>
> This changes the result of e.g. to_char(-7, '9.MI') from '7' to '7-'
> compared with released branches. I think that is the correct result, but
> you may prefer to handle the back-branches differently.
>
> A regression test that would have caught it:
>
> SELECT octet_length(to_char(7, '9.')), to_char(-7, '9.MI'),
> to_char(-7, '9.PR');
>
> We found this with a cross-version differential fuzzer (random to_char,
> to_number and to_date calls on REL_19_STABLE vs master), written for a
> university course project (CS-5187).
>
> Regards,
> Gowtham
>
>

Attachment Content-Type Size
v1-0001-Fix-to_char-for-number-formats-ending-in-a-decima.patch application/octet-stream 3.2 KB

In response to

Responses

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-10-18 23:22:05 BUG #19772: ERROR: corrupt MVNDistinct entry for GROUP BY c0, ROLLUP (abs(c0)) with ndistinct statistics
Previous Message PG Bug reporting form 2026-10-18 20:40:57 BUG #19771: to_char(): NUL byte in result for formats with a trailing decimal point (regression from ec352b37d)