flan/test/noinstall.c
Joseph Ferano b54f24873e The job ring never looked at tail, and the comment described a drop it never did
publish() wrote queue[head % QUEUE] without consulting tail, so the 65th module
queued between two agent/poll calls landed on the slot the game thread was
reading — twenty-four bytes of function pointers copied field by field with no
atomic near them, so the consumer could take half of one job and half of
another and call it. The comment claimed the overflow dropped the oldest
request; nothing did that.

A full ring is refused now, at the sender, before the dlopen. Dropping loses a
reload the sender was told was ok, which is the same lie more quietly; blocking
stalls the accept loop, which serves connections inline, so a program that had
stopped polling would also stop answering status and abort — the dev loop would
have no way to reach a program that had stopped listening to it. The check is
separate from the store because there is one producer: room, once seen, cannot
be taken away.

Two smaller defects in the same file:

A module with no flan_reload_install was refused and its handle dropped on the
floor. Not an exception to "nothing is ever dlclosed" — that rule is about a
module something points into, and this one installed nothing, so no cell names
it. What leaked was the handle value rather than the mapping: dlopen refcounts
by path, so re-sending the same bad file raised a count nothing could lower.

exit(134) from the break loop runs the atexit chain and the ELF destructors,
which want the loader lock the listener thread may be holding inside dlopen. A
program asked to abort would hang instead of dying. _exit, with the streams
flushed by hand at each call site. The deadlock itself is read rather than
tested; what the tests pin is that the exit status is still 134.

programs/agent-queue.flan blocks on stdin so the window is held open by the
test rather than by a timer: it takes 64 modules, refuses the 65th with a
reason, and installs 64 when it finally polls. noinstall.c's destructor prints
while the program is still running, which is the only way to see the close — at
exit the loader runs every destructor whether anything was closed or not. Both
halves fail on the old code.
2026-09-12 10:39:46 +07:00

17 lines
793 B
C

/* A shared object that is not a redefinition module: it loads, and it has no
* flan_reload_install for the agent to find.
*
* The destructor is the observation. The agent used to drop the handle on the
* floor when it refused a module like this one — the mapping stayed, the
* reference count went up, and the one handle that could have brought it down
* was gone. Now the module is closed, because nothing was installed from it
* and so nothing can point into it, and this line appears while the program is
* still running. Waiting for the program to exit would prove nothing: the
* loader runs every destructor at exit whether anything was closed or not. */
#include <stdio.h>
__attribute__((destructor)) static void unloaded(void) {
printf("unloaded\n");
fflush(stdout);
}