Bug#919369: libc6: exit() rewinds fd 0, which causes impredictable behaviour in case of fork()
Package: libc6
Version: 2.28-5
Severity: normal
Dear Maintainer,
*** Reporter, please consider answering these questions, where appropriate ***
* What led up to the situation?
* What exactly did you do (or not do) that was effective (or
ineffective)?
* What was the outcome of this action?
* What outcome did you expect instead?
*** End of the template - remove these template lines ***
it appears that exit() flushes not only output streams but also stdin.
In the case of stdin, the flush appears to be an
lseek(ftell(stdin) - lseek(0, 0, SEEK_CUR), 0, SEEK_CUR);
This seeks the fd 0 to the point last read in stdin by stdio functions.
This becomes a problem when the program forks. Indeed after
fread(buf, 4095, 1, stdin);
each
if (!fork()) exit(0);
rewinds fd 0 by one byte. I would expect that the previous line had no
effect (except some CPU time consumption). The child process does not
access stdin (or fd 0) and thus it sounds reasonable that it does not
change the file descriptor offset.
To illustrate the behaviour, here is a sample C program
---8<------8<------8<------8<------8<------8<------8<------8<---
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int
main(int argc, char **argv)
{
char buf[8192];
int type=0;
if (argc > 1)
type = atoi(argv[1]);
fread(buf, 1024, 6, stdin);
if (type && !fork()) {
sleep(1);
if (type >= 2)
fclose(stdin);
exit(0);
}
if (type && !fork()) {
sleep(2);
if (type >= 2)
fclose(stdin);
exit(0);
}
if (type && !fork()) {
sleep(3);
if (type >= 2)
fclose(stdin);
exit(0);
}
printf("%ld\n", lseek(0, 0, SEEK_CUR));
wait(NULL);
printf("%ld\n", lseek(0, 0, SEEK_CUR));
wait(NULL);
printf("%ld\n", lseek(0, 0, SEEK_CUR));
wait(NULL);
printf("%ld\n", lseek(0, 0, SEEK_CUR));
exit(0);
}
---8<------8<------8<------8<------8<------8<------8<------8<---
Compile it as seektest and do a dd if=/dev/zero bs=1024 count=8 of=inp.
The program reads 6kb of data, (conditionally) creates three children
and and exits in each one. It prints the offset of fd 0 before the
first exit and after each exit. The argument modifies the behaviour of
the program. If it is 0 (or absent), then there is no fork(), if 1 or
2, the program forks/exits three times and if it is 2 it
fcloses(stdin) in each child before exiting.
Usage:
No fork:
./seektest 0 < inp
3 forks:
./seektest 1 < inp
3 forks, with fclose(stdin) before exit:
./seektest 2 < inp
In the first and last case the result is
8192
8192
8192
8192
which is what I expect. In the second case one gets
8192
6144
4096
2048
which does not look correct.
In addition, here is a random number generator
---8<------8<------8<------8<------8<------8<------8<------8<---
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int
main(void)
{
pid_t pid = getpid();
char buf[4096];
signal(SIGCHLD, SIG_IGN);
fread(buf, 4095, 1, stdin);
fork(); fork(); fork();
fork(); fork(); fork();
fork(); fork(); fork();
if (pid == getpid()) {
usleep(2000);
printf("%ld\n", lseek(0, 0, SEEK_CUR));
}
exit(0);
}
---8<------8<------8<------8<------8<------8<------8<------8<---
that you can compile as rndseek and use with
rndseek < inp
(the generator is not very good as random number generator but it
illustrates that the behaviour is not what one should get).
-- System Information:
Debian Release: buster/sid
APT prefers oldoldstable-updates
APT policy: (500, 'oldoldstable-updates'), (500, 'oldoldstable'), (500, 'unstable')
Architecture: amd64 (x86_64)
Foreign Architectures: i386
Kernel: Linux 4.12.0-2-amd64 (SMP w/8 CPU cores)
Locale: LANG=fr_FR.UTF-8, LC_CTYPE=fr_FR.UTF-8 (charmap=UTF-8), LANGUAGE=fr_FR.UTF-8 (charmap=UTF-8)
Shell: /bin/sh linked to /bin/dash
Init: systemd (via /run/systemd/system)
Reply to: