Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <871pgyuf5l.fsf@gmail.com>
Date: Wed, 01 Apr 2026 22:04:06 -0700
From: Collin Funk <collin.funk1@...il.com>
To: musl@...ts.openwall.com
Cc: Bruno Haible <bruno@...sp.org>
Subject: standard output is always line buffered for the first line

Hi,

POSIX states the following about standard output [1]:

    When opened, stderr shall not be fully buffered; stdin and stdout
    shall be fully buffered if and only if the file descriptor
    associated with the stream is determined not to be associated with
    an interactive device.

This is not the case on musl, where the first line is always line
buffered.

Here is a test program:

    $ cat main.c
    #include <stdio.h>
    #include <unistd.h>
    int
    main (void)
    {
      printf ("%s %s\n", "hello", "1");
      sleep (1);
      printf ("%s %s\n", "hello", "2");
      sleep (1);
      printf ("%s %s\n", "hello", "3");
      return 0;
    }

Here is some strace output on glibc showing that standard output is
fully buffered:

    $ gcc main.c && strace ./a.out > /dev/full
    [...]
    fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x7), ...}) = 0
    ioctl(1, TCGETS2, 0x7ffd3cccb940)       = -1 ENOTTY (Inappropriate ioctl for device)
    brk(NULL)                               = 0x24e0a000
    brk(0x24e2c000)                         = 0x24e2c000
    clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffd3cccbd10) = 0
    clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffd3cccbd10) = 0
    write(1, "hello 1\nhello 2\nhello 3\n", 24) = -1 ENOSPC (No space left on device)
    exit_group(0)                           = ?
    +++ exited with 0 +++

Here is the strace output on musl, where we can see the first line is
line buffered before standard output is made fully buffered:

    $ gcc main.c && strace ./a.out > /dev/full
    [...]
    ioctl(1, TIOCGWINSZ, 0xbfee27e8)        = -1 ENOTTY (Not a tty)
    writev(1, [{iov_base="hello 1", iov_len=7}, {iov_base="\n", iov_len=1}], 2) = -1 ENOSPC (No space left on device)
    nanosleep({tv_sec=1, tv_nsec=0}, 0xbfee29e0) = 0
    nanosleep({tv_sec=1, tv_nsec=0}, 0xbfee29e0) = 0
    writev(1, [{iov_base="hello 2\nhello 3\n", iov_len=16}, {iov_base=NULL, iov_len=0}], 2) = -1 ENOSPC (No space left on device)
    exit_group(0)                           = ?
    +++ exited with 0 +++

This was found by some failing tests in GNU coreutils [2]. Without
sharing a bunch of unnecessary details, it is the reason for this
difference in behavior:

    $ ldd --version | head -n 1 && date > /dev/full
    ldd (GNU libc) 2.42
    date: write error: No space left on device
    $ ldd 2>&1 | head -n 2 && date > /dev/full
    musl libc (x86_64)
    Version 1.2.5
    date: write error

I'll copy some text that Bruno Haible wrote on coreutils@....org [3]:

> To understand what happens, set these breakpoints in gdb:
>   (gdb) break __stdio_write
>   (gdb) watch stdout->lbf
> 
> In musl/src/stdio/stdout.c:9 stdout is initialized to be initially
> line-buffered.
> 
> Then, during the first printf call, the pieces of the format string
> are stored in the buffer (via the memcpy() in __fwritex). But for the
> last piece, the newline character, this code in musl/src/stdio/fwrite.c
> is executed:
> 
>         if (f->lbf >= 0) {
>                 /* Match /^(.*\n|)/ */
>                 for (i=l; i && s[i-1] != '\n'; i--);
>                 if (i) {
>                         size_t n = f->write(f, s, i);
> 
> This invokes the __stdout_write function, which executes
> 
>         if (!(f->flags & F_SVB) && __syscall(SYS_ioctl, f->fd, TIOCGWINSZ, 
> &wsz))
>                 f->lbf = -1;
> 
> The F_SVB flag is unset, since setvbuf() has not been executed on this FILE
> stream. The ioctl syscall returns non-zero, and thus the 'lbf' field gets set
> to -1, switching the stream from line-buffered to fully buffered.
> 
> But at this point the caller (__fwritex) has already decided to output the
> first line, and this happens through the subsequent __stdio_write call.
> 
> I believe this is a musl libc bug. __fwritex should trigger switching
> the stream from line-buffered to fully buffered *before* deciding whether
> to output a line, not after doing this decision. In other words, the
> assignment
>   f->lbf = -1;
> ought to be performed *before* the test
>   if (f->lbf >= 0)

Collin

[1] https://pubs.opengroup.org/onlinepubs/9799919799/functions/stdin.html
[2] https://lists.gnu.org/archive/html/coreutils/2026-03/msg00097.html
[3] https://lists.gnu.org/archive/html/coreutils/2026-04/msg00005.html

Powered by blists - more mailing lists

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.