2026-09-02 - (/loop) Bulk port batch 14: the timed acquisitions, and a guard that checked the wrong table

documented   715 needed, 239 missing   ->   715 needed, 225 missing

Fourteen names: seven functions and the posix_-prefixed twin of each. This was primitive work before glue, as expected - and it turned up two things that were not on the list.

The primitive work

sync::cond_wait already took a timeout; nothing else did. The mutex said "how patient are you" by having a second entry point, the semaphore by having a third, the read-write lock by a bare bool. A deadline would have been a fourth spelling of one idea, so it became one instead:

pub enum Blocking { Never, Forever, Until(Instant) }

lock/try_lock collapsed into acquire, semaphore_wait/semaphore_try_wait into semaphore_wait, and the two read-write acquisitions lost their bool. Every blocking loop in the module now goes through one wait_while helper. All 33 existing tests passed unchanged afterwards, which is what says the collapse preserved behaviour rather than merely compiling.

TryLock became Acquisition, because a type named after the impatient case was about to be returned by a call that may block forever. Busy now means "did not get it" and the caller knows why from the patience it asked for - which is why timing out is not a fourth variant.

The deadline is re-read every turn, and that is the whole of the difficulty. Handing the full remaining span to each wait_timeout restarts the clock on every spurious wake, and this module's releases notify all waiters, so a contended lock would be waited on forever by a call that was asked to give up in a millisecond.

The functions

pthread_mutex_timedlock, pthread_rwlock_timedrdlock, pthread_rwlock_timedwrlock, sem_timedwait, sem_getvalue (POSIX.1-2008), and sem_reltimedwait_np, pthread_cond_reltimedwait_np (Solaris sem_timedwait(3C) and pthread_cond_timedwait(3C), which document both spellings and their arities).

The POSIX calls take an absolute deadline; the _np pair take a relative span. Two readers, not one with a flag, because neither misreading fails loudly: an absolute time read as relative restarts the clock every retry and the loop never ends, and a relative span read as absolute expires instantly, because one second is a moment in 1970.

Two things that were not on the list

A timeout had no error code, and the honest fix was a decision (D476)

ETIMEDOUT has never been seen from the target, and the errno module said in as many words that everything in it had been. Answering the measured-but-wrong EBUSY would make a retry loop spin forever; answering the placeholder would report "not handled" for a call that handled it exactly. So the module now carries a published tier beside the measured one, with the line between them stated - and TIMED_OUT names the one conformance check that would promote it. Nothing was loosened to let it in.

A guard was checking a different table from the one it named

no_name_is_delegated_twice said:

A duplicate would mean the registry's last-wins rule picks one silently.

...and then checked the delegation table, where a duplicate is harmless because both rows are identical. The list that rule actually applies to is the module's declarations, and three names were duplicated there: pthread_cond_timedwait and pthread_attr_setschedpolicy each had a live arity shadowed by a stale zero left behind in the not-served list, and sched_yield was declared twice over.

So batch 13's pthread_cond_timedwait, whose comment says in capitals that it takes an absolute deadline as its third argument, was registered as taking none. Nothing observable broke - arity is metadata for the trace and the gap report, not for dispatch - which is exactly why it survived. It is invisible until somebody reads a trace of a timed wait and finds it took no arguments.

Fixed, and no_name_is_declared_twice now guards the right list. Both guards were watched to fail before being trusted: a duplicate was reintroduced and each named its offender.

A hanging test is not a failing test

The test that matters most here is the one for the deadline arithmetic: readers churn in and out while a writer waits, so every release wakes it with nothing. The arithmetic was reverted deliberately to check the test caught it.

It did not fail. It hung - which is the honest symptom of an unbounded wait, and useless as a guard, because a hanging test stops the suite and reports nothing about what broke. Rewritten to do the wait on its own thread behind a channel, so the deadline that decides the verdict is the one on the recv. Reverted again: it now fails in five seconds naming the cause.

The gates I had not been running

Every batch worklog since 309 ends "clippy clean, fmt clean, kernel/posix tests pass". All of that was true. It was also not the whole test suite, and this tick finally ran cargo test --workspace - which fails, and has been failing for some time.

The cross-crate guards live in orbistoun-service, because they are the only ones that can see more than one subsystem at once. Running -p orbistoun-kernel never reaches them. So the narrow command passed, the worklog reported what the narrow command said, and the reader was left to assume it meant more than it did.

That is the same over-claim principle 3 keeps catching, one level up: not a wrong measurement, a true measurement reported as a broader one. Worklogs 310-314 should be read as "the crate tests passed", which is what was actually checked.

Four failures, from three causes. One was mine and is fixed; two are not fixed here, and each says why.

Fixed: posix_pthread_barrier_init was implemented but declared nowhere

Batch 11 split the POSIX-signature barrier init from the vendor one (D385) and wrote in the exception list that it was "declared in the POSIX module". It was not. The POSIX module declares pthread_barrier_init; the internal entry point it delegates to had no declaration at all, so nothing reached the registry under that name.

Its own twin showed the fix: posix_pthread_rwlock_init is declared in libkernel alongside the vendor spelling it splits from. The barrier init is now declared the same way, and the exception - which was excusing something on a false premise - is gone.

Not fixed: sysctlbyname is implemented twice, in two crates

orbistoun-kernel and orbistoun-libc each declare and implement it. A NID is the hash of a name alone, so both resolve to one NID and the registry's last-wins rule picks one silently.

This predates today - the symbol appears on neither side of the day's diff, so both implementations were there at the last commit, and the three guards that trip on it (no_symbol_is_declared_twice, nids_differ_per_symbol, and the declared-count check) were already red.

It is left for its own tick because it is a merge, not a deletion. The first look said the libc one was a strict superset - it answers three measured integer knobs (hw.ncpu, hw.pagesize, machdep.tsc_freq, all read off a console), plus kern.ostype, plus it reports unknown names once so they become a work list. That was wrong on one point: the two differ on an unset kern.osrelease, where libc refuses and the kernel answers an empty NUL-terminated string - "a knob that exists with no value, rather than an invented one", which is D447's reasoning and is the behaviour an obSCEne conformance case checks. Deleting either one drops something deliberate. Doing that carelessly at the end of a tick is how a hardware-verified check quietly regresses.

Not fixed: 112 implemented functions have no knowledge entry

every_implemented_function_is_written_down exists because "implementing something without recording what was learned is how the knowledge ends up existing only in a conversation". It is red, and the count is the point: 112 functions, of which 84 were registered today by the bulk batches and 28 predate them. So this gate was red before this run started, and the bulk port made it four times worse without noticing.

Not backfilled here on purpose. Each entry wants a real purpose, a real provenance and real assumptions, and 112 formulaic rows written at speed would satisfy the guard while defeating what it is for. The material exists - every one of these has a doc comment stating its specification and a worklog entry - so the honest version is a pass that derives each entry from what is already written, which is a unit of work rather than a footnote to this one.

That is the next tick, ahead of more porting: the gap number is worth less than the record behind it, and continuing to add functions while the guard that records them is red only widens the hole.

State

clippy --tests clean, fmt clean, identity scan clean, nothing committed.

Kernel and posix crate tests pass (39 in the sync suite, six of them new). cargo test --workspace does not - four failures, all described above, one of them fixed since and three remaining across two causes that both predate this batch.

Next: 225, but not by porting. The knowledge backfill and the sysctlbyname merge come first, because both are guards this project already wrote and is currently ignoring. After them: the socket scatter/gather (sendmsg, recvmsg, sendto, recvfrom) can reuse the iovec reader from readv/writev, though msghdr is a different structure and its layout has to be read rather than assumed. Thread cancellation still needs a design rather than a spec reading - storing the state without ever delivering a cancellation would be a lie, and saying so is the minimum if only the storage lands.