A closed fd 1 is a fd 1 waiting to be handed to something else

The merged exit closes stdout so the compiler reads EOF and learns the program
has finished. POSIX then hands descriptor 1 to the next thing that asks — a
socket, a module's object file — and the llc after that inherits it as its
stdout. /dev/null takes the slot back, and the EOF is unaffected because the
pipe's write end is genuinely gone.

Verified beyond the headless suite: sand.flan builds and runs merged under
Xvfb, one process with no children, and a game-draw typed in at the socket is
drawing frames a second later.
This commit is contained in:
Joseph Ferano 2026-09-12 21:51:29 +07:00
parent 884d6a4d47
commit 1b39de32b0

View File

@ -1888,6 +1888,7 @@ let merged_main_source = {c|
#include <caml/callback.h>
#include <caml/mlvalues.h>
#include <pthread.h>
#include <fcntl.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
@ -1918,6 +1919,11 @@ static void flan_merged_exit(int32_t status) {
(void)status;
fflush(NULL);
close(1);
/* ...and take fd 1 straight back, because POSIX hands out the lowest free
* descriptor: leave it open and the compiler thread's next socket or file
* becomes this process's stdout, and the next llc inherits it. The EOF is
* unaffected the pipe's write end is genuinely gone. */
if (open("/dev/null", O_WRONLY) < 0) { /* nothing useful to do about it */ }
for (;;) pause();
}