Wrap process control: fork, exec*, waitpid, posix_spawn, the environment
#11
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Source: TODO.md §4.1 (at
669b2b3)fork, theexec*family,waitpid,waitposix_spawnsystem,popen,pclosegetpid,getppid,getuid,geteuid,setuid,setgidatexit,exit,_exit,abort-- mostly to give akerror a shutdown hookgetenv,setenv,unsetenv,putenv,clearenvforkneeds thinking about before it is wrapped: the error pool is inheritedby the child, and any context live at the moment of the fork exists twice
afterwards. That is a decision to make deliberately, not something to discover
from a double release.
Files:
src/,include/akstdlib.hFiled by Tachikoma (Claude Code, Opus 5, 1M context)
@tachikoma let's get a better plan on this
Plan
This ticket is six checkboxes covering roughly twenty calls, and they do not share
a decision. One of them is blocked on
libakerrorand the rest are not, so as oneissue it cannot leave
status::groominguntil the hardest item is settled — whichholds up the easiest. Steps:
forkquestion, and file what it needs upstreamsystemandpopenare forSplit it
getenv,setenv,unsetenv,putenv,clearenvgetenvreturning NULL is successgetpid,getppid,getuid,geteuid,setuid,setgidatexit,exit,_exit,abortfork,exec*,posix_spawn,wait,waitpidsystem,popen,pcloseRecommendation: five issues. The environment piece is a clean afternoon and
has no open question; it should not wait on
fork.The
forkquestionThe ticket says a context live at the fork exists twice afterwards. Worth being
precise about what that does and does not cost, because the phrase "double
release" suggests a sharper failure than the one that is actually there.
AKERR_ARRAY_ERRORis a fixed static array (akerror.tmpl.h:103). Afterforkthe child gets its own copy-on-write duplicate, not shared memory. So parent and
child each release their own copy and there is no cross-process double free.
The real cost is narrower and quieter: the child's pool comes up holding slots
that describe the parent's in-flight work. Nothing in the child will ever
release them, because the code that would have is in the other process. If the
child
execs or_exits immediately, this costs nothing. If the child keepsrunning — which is the whole point of
forkwithoutexec— it has permanentlylost those slots, and the pool is bounded.
That makes the fix a pool reset in the child between
forkreturning 0 and thewrapper returning. libakerror exposes no such call.
akerr_init()is the onlything close and it reserves status ranges as a side effect, so calling it twice is
not obviously safe.
Recommendation: file an issue against
libakerrorfor a post-fork pool reset —either an explicit
akerr_reset_pool()or apthread_atforkchild handlerinstalled by
akerr_init(). AGENTS.md is explicit that a defect in a dependencyis filed against that dependency, and libakerror already carries #14 (
IGNORE()leaks a pool slot), which is the same resource under a different failure. Until
that lands,
forkstays unwrapped andTODO.md's "Not wrapped" table says why.An
atforkhandler is the better of the two: it is correct for a caller who callsfork(2)directly, and this library cannot make that caller go away.posix_spawnis worth calling out as the unblocked half of this piece. It neverreturns twice, so the pool question does not arise, and it covers the
fork-then-exec case that is most of the real uses. It could ship while
forkwaits.
The "cannot fail" question
getpid,getppid,getuidandgeteuidare defined never to fail. Theirwrappers would take no pointer that can be NULL beyond the out-param, and raise
nothing but
AKERR_NULLPOINTER.This is the same question as
rewinddirin #10 andumaskin #9, and itshould get one answer for all three. Two defensible ones:
writing
aksl_everywhere should not have to remember which four calls arespelled differently.
ceremony, and
TODO.md's "Not wrapped" table exists precisely to record this.I lean toward wrapping, on the consistency argument — the cost is a few lines
each and the alternative is a caller checking the header every time. But it is a
taste call, not a correctness one, and it should be made once and written down
rather than settled per-function.
setuidandsetgidgenuinely fail (EPERM) and are not part of this question.systemandpopenThese take a shell command string, so every caller is one interpolation away from
a command injection, and
systemadditionally blocksSIGINT/SIGQUITin waysthat surprise people.
popenalso returns aFILE *that must be closed withpcloserather thanfclose— andaksl_fcloseexists and will happily accept it. That is a realtrap this library would be introducing: a wrapper family where two members
return interchangeable-looking handles that need different close calls.
Recommendation: wrap
popen/pclose, and do not wrapsystem.popenhasuses that nothing else covers;
systemisposix_spawnplus a shell, and theshell is the part that hurts. If
popenlands,aksl_pcloseneeds a loud@warningon both it andaksl_fclose.Sequencing
atexitis what would give akerror a shutdown hook, so itis worth doing before the pool work rather than after
popen/pclose— independentposix_spawn,wait,waitpid— independent of the pool questionforkandexec*— after the libakerror issue landsWhat I would change on the ticket
Close this one as split, or keep it as the tracking issue and open the five
underneath it. Either way the
forkpiece should carrystatus::blockedwith alink to the libakerror issue, so it is visible that it is waiting on something
rather than unloved.
I can file the libakerror issue and the split issues on request — say the word and
I will, rather than doing it unasked.
— Tachikoma (Claude Code, Opus 5, 1M context)