D667 - The vendor spelling of a socket call, and the errno that travels with it

Status: decided Date: 2026-09-10

Five checks failed for one reason, and the reason was a table

obSCEne under orbistoun answered 0x7fff0001 to sceNetSocket, sceNetBind, sceNetListen, sceNetSetsockopt, sceNetRecv and sceNetSend. Every one of those has had a working body in orbistoun-fs for months - under its POSIX name. The vendor twins were declared in orbistoun-net and implemented nowhere, so a title reaching the network hit stubs while a payload doing the same thing worked.

The first attempt was to add the vendor names to orbistoun_fs::socket::implementations(). It changed nothing, and the reason is worth recording: that table is consumed by orbistoun_posix::implementations(), which uses it as a lookup source for its DELEGATED pairs and returns only POSIX names. Entries added there are visible to the lookup and invisible to the dispatcher. A table that is read but not returned is a good way to spend an hour.

Where a vendor name is offered

The crate that declares a library offers its names, wherever the body lives. That is already D525's rule for sceKernelStat - body in metadata, name declared and offered in orbistoun-fs - and it decides this cleanly: orbistoun-net declares libSceNet, so orbistoun-net offers sceNet*, and takes a dependency on orbistoun-fs for the bodies.

orbistoun-fs owns sockets because it owns the descriptor table, and a guest closes a file and a socket with the same close. Splitting the two tables would give a guest two numbering spaces. So the bodies stay put and the spelling moves.

The two spellings disagree about exactly one thing

A POSIX-named socket call answers -1 and leaves the number in errno. libSceNet folds it into the return: 0x8041_0100 | errno (D627). Serving one body under both names means the success paths agree and the failure paths do not - which is why the vendor names could not simply be aliased onto the POSIX functions.

So the bodies stopped collapsing to -1. Each returns Answer = Result<u64, u32> - a value, or the POSIX errno that stopped it - and each crate encodes it its own way: orbistoun_fs::socket::as_posix and orbistoun_net::socket::as_vendor. Two small macros generate the wrappers, one per crate, because the rules differ and neither crate should have to learn the other's.

A failure nothing can name is errno zero. Zero is what errno holds when nothing went wrong, so 0x8041_0100 can never collide with a measured code, and a guest that switches on the result meets a code it does not know - which is exactly true. Answering a plausible EINVAL would be inventing a constant to make a failure look explained.

Three things the measurement settled that guessing would not have

sceNetSocket takes a name before the address family. obSCEne types the import as int (*)(const char *, int, int, int); POSIX socket has no such argument. Sharing the body unshifted reads a const char * where AF_INET belongs and refuses every socket. Every other call in this library does sit at the POSIX positions, which is what makes the one exception easy to miss - and it is now asserted from both sides, because only the pair says which way round it is.

setsockopt(SOL_SOCKET, 0x1200, &1, 4) is how this platform turns non-blocking on, and it is applied rather than accepted. 0x1200 is not the FreeBSD SO_* numbering and no harvested header names it; hardware answered 0x0 to it and -1 to fcntl(F_SETFL, O_NONBLOCK) on the same machine, and refused the 0x1100 the check offers as a fallback. That it is applied is a second measurement: a recv on that socket answers EAGAIN, which a blocking socket cannot produce. The shim had been accepting it and doing nothing, which is the plausible answer.

ENOTCONN is 57 and it is now a measured entry. sceNetRecv on a listener answers 0x8041_0139, on a connected empty socket 0x8041_0123. Both decode under the base D627 established, and orbistoun now answers the same two codes for the same two conditions.

MSG_DONTWAIT was not a wrong answer, it was a hang

The first run with the vendor names bound got worse: 222 imports and 18,383 calls against 245 and 450,179, with the guest silent for the last nine seconds. 102-net/recv-would-block connects a plain blocking socket and reads it with MSG_DONTWAIT; the shim ignored flags, so the read waited, and the run's time limit ended the guest with eleven sections unrun.

A shim that answers wrongly costs one check. A shim that blocks costs the rest of the run. That is a distinction worth keeping in view when deciding whether ignoring a flag is acceptable - and it is why the Wait type pairs "force non-blocking for this call" with "put it back" in one value rather than two statements a reader has to notice are paired.

Putting it back correctly needed the mode remembered on the socket, because the host has a setter and no getter. Doing that turned up the same defect already shipped: the readiness peek behind select set non-blocking, probed, and restored false unconditionally - handing a guest's non-blocking socket back blocking. Found while fixing the other one.

What it bought

Every 102-net check now passes, and each one matches the value hardware answered rather than merely reporting success:

check hardware orbistoun
sockaddr-bind pass 0x0, sin_len 0 and 16 same
nonblocking-option pass 0x1200 same
recv-would-block pass 0x80410123, listener 0x80410139 same
accept-inherits pass 0x80410123 same
listener pass, descriptor 0x13 pass, descriptor 0x4

The descriptor number is the one honest difference: it is each system's own numbering, and orbistoun's table starts at 3.

Across the whole sweep, 191 checks passing became 196 with nothing regressing, and two 900-surface rows moved from fail to partial.

One divergence this opened

Hardware answers NULL to dlsym for these names; orbistoun now resolves them, because implementing a name is what makes it resolvable here. Not a failed check - 102-net/resolve passes either way - but it is a real difference in what a guest can discover about itself, and it is recorded rather than left to be rediscovered.