D385 - What a harvest skips has to be counted, or the section is a lie
assumed - 2026-08-30
zftpd serves FTP under orbistoun. A client connects,
logs in, and gets answers:
220 Service ready for new user.
230 User logged in, proceed.
215 UNIX Type: L8
200 Type set.
Six things stood in the way. Each is worth its own paragraph, and one of them is a rule about this project's own tooling rather than about the platform.
The harvester had taken none of the event filters
sys/sys/event.h writes every filter as
#define EVFILT_READ (-1), which is what a header does with
a negative constant so EVFILT_READ - 1 cannot mean
something else. The harvester required bare digits, so it took
EVFILT_SYSCOUNT and nothing else - the one
number in the set that is not a filter. The section existed, looked
harvested, and named nothing a guest can ask for.
That is the third time a rule about spelling silently took the wrong set:
| rule | what it dropped | how it surfaced |
|---|---|---|
| upper case throughout (D378) | 599 of 600 syscall numbers | the count |
| C octal is not TOML octal (D374) | every constant in every section | a parse failure |
| bare digits only | all fifteen event filters | the count |
Three times, and each time the count was the only thing that
said so. So the rule is now: a harvest reports what it skipped,
by name, per section. A #define whose name qualifies and
whose value does not is a decision, and a decision nobody can see is one
nobody can check.
The edge test made it worse rather than better. It asserted
!is_plain_number("-1") - written from the rule rather than
from what a header contains - so the test protected the bug. A
test that restates the implementation is not a test.
The table was in a crate that could not be reached from underneath
abi_constant lived in orbistoun-libc, which
depends on orbistoun-fs. So the crate that implements
sockets, files and now event queues could not read the harvested
numbers, and wrote its own down by hand with a citation in a comment -
pub const AF_INET: u64 = 2; - checked by a test in the
crate that could read the table, comparing two copies.
That is the retyping the harvest exists to prevent, wearing a comment
as a disguise. The table moved to orbistoun-hle, which is
below both, and the families are read where they are used. The test
comparing two copies is gone because there is one copy, and what
replaced it asserts the reader rather than the agreement.
A POSIX name and its vendor twin can have different arities
libScePosix delegates most POSIX names straight to their
vendor-named twin, and takes the arity "from the vendor-named function
each delegates to, so the two cannot disagree". They disagree. Three of
the vendor calls end in a name the POSIX ones have no
argument for:
| POSIX | vendor |
|---|---|
pthread_create(thread, attr, start, arg) |
scePthreadCreate(..., name) |
pthread_cond_init(cond, attr) |
scePthreadCondInit(..., name) |
pthread_mutex_init(mutex, attr) |
scePthreadMutexInit(..., name) |
So a guest calling the POSIX spelling had an uninitialised
argument register read as a string pointer. zftpd
had bound its socket and listened on it and was initialising its client
table when pthread_mutex_init read rdx, which
held 0x18 left over from the loop above, and faulted on
it.
Nothing detects this by inspection. The delegation resolves, the test that every delegation names a real implementation passes, and the call works perfectly for every guest that happens to leave a readable address in that register. Making two things agree by construction is not the same as making either of them right.
fcntl
is a pair, which makes an unimplemented answer worse
fcntl(5, F_GETFL) -> 0x7fff0005 an orbistoun placeholder
fcntl(5, F_SETFL, 0x7fff0005) handed straight back
close(5)
D125 in its purest form: a function answering an error code where a caller expects data, and the damage landing one call later under a different name. A guest reads flags, changes one bit, and writes them back, so a placeholder does not stay where it was put.
O_NONBLOCK is honoured because a server's event loop
depends on it. FD_CLOEXEC is remembered and does nothing,
which is honest rather than lazy: nothing here ever execs,
so there is no moment at which it could have an effect, and a guest that
sets it and checks it is still entitled to a consistent answer.
A
marker in a .bss global is a diagnostic changing the
program
Entering past the runtime fills the globals that runtime would have filled, and gives every unserved one a marker instead of the null the loader left - so its first use names it rather than faulting on zero. That is the right answer for a payload whose unserved globals are a handful of kernel addresses a loader supplies.
zftpd has 126 named globals and 24 that nothing here
implements. It printed
Server running. Press Ctrl+C to stop. and then
Shutting down... in the same breath, having made no call in
between - because a marker is non-zero, and one of those globals is read
by a while.
A diagnostic that changes the program is not a
measurement (principle 3, D227). The function's own doc comment
claimed it left unserved names null; the code did the other thing, and
the two had disagreed for as long as both existed.
ORBISTOUN_RUNTIME_GLOBALS=zero now means what the comment
said, and under it zftpd stays up and serves.
Where the five core payloads stand
| payload | port | how far |
|---|---|---|
zftpd |
2120 | serves - banner, login, SYST,
PWD, TYPE |
klogsrv |
3232 | accepts a connection and holds it |
shsrv |
2323 | accepts, then drops: it wants to exec
a shell |
ftpsrv |
2121 | stops at the AuthID wall - it raises kernel privileges first (D382) |
elfldr |
9021 | dies in __crt_start; no main symbol to
enter at (D326) |
pldmgr |
8084 | the same |
CWD / answers 550 Not a directory., which
is the filesystem's root: / is no mount prefix, so the
first thing an FTP client does has nowhere to go. That is now a measured
work item rather than a question about what a console's directory tree
looks like - the server will say which paths it wants, in order, as soon
as it can list one.