diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32a25a1..e6206d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,7 +180,7 @@ jobs: - name: Every interface, every kind of examination run: | - bash .spec/tools/run-conformance.sh openkal-macos . full + bash .spec/tools/run-conformance.sh openkal-macos . full,optional # The other architecture, as far as this system allows it to be reached. # diff --git a/.github/workflows/numbers.yml b/.github/workflows/numbers.yml index 829296c..9bbb462 100644 --- a/.github/workflows/numbers.yml +++ b/.github/workflows/numbers.yml @@ -49,7 +49,11 @@ jobs: ulock_wait ulock_wake __ulock_wait __ulock_wake \ futimens utimensat futimes utimes settimeofday \ fchmod fchmodat fcntl \ - pipe sigaction; do + pipe sigaction \ + socket connect accept accept_nocancel bind listen shutdown \ + getsockname getpeername setsockopt getsockopt \ + sendto recvfrom sendmsg recvmsg socketpair \ + poll select pselect kevent; do printf '%-24s' "$n" grep -E "^#define[[:space:]]+SYS_${n}[[:space:]]" "$h" | head -1 || echo '(absent)' done @@ -138,6 +142,21 @@ jobs: { "mkdirat", 475, -2, 0, 0, 0 }, { "ulock_wait", 515, 0, 0, 0, 0 }, { "ulock_wake", 516, 0, 0, 0, 0 }, + /* The socket calls, with a descriptor that cannot be one. */ + { "recvfrom", 29, -1, 0, 0, 0 }, + { "accept", 30, -1, 0, 0, 0 }, + { "getpeername", 31, -1, 0, 0, 0 }, + { "getsockname", 32, -1, 0, 0, 0 }, + { "select", 93, 0, 0, 0, 0 }, + { "socket", 97, 0, 0, 0, 0 }, + { "connect", 98, -1, 0, 0, 0 }, + { "bind", 104, -1, 0, 0, 0 }, + { "setsockopt", 105, -1, 0, 0, 0 }, + { "listen", 106, -1, 0, 0, 0 }, + { "getsockopt", 118, -1, 0, 0, 0 }, + { "sendto", 133, -1, 0, 0, 0 }, + { "shutdown", 134, -1, 0, 0, 0 }, + { "poll", 230, 0, 0, 0, 0 }, }; int main(void) { diff --git a/README.md b/README.md index dbb5e96..7929863 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ written on the kernel's own calls. ```toml [dependencies] -openkal = "0.5.1" +openkal = "0.8.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.3.1" +openkal-macos = "0.5.0" ``` Its purpose is as much to test the specification as to be used. A specification @@ -18,9 +18,31 @@ different system is what turns the claim into an observation. ## Interfaces provided -All eight. `tools/check-surface.sh --complete` in the specification package +All fifteen. `tools/check-surface.sh --complete` in the specification package compares the exported names against `SURFACE.txt`. +The five that openkal 0.8 added were declined by this implementation until now, +and each is provided in whole: + +| | on this system | +| --- | --- | +| `openkal.net` | the kernel's own socket calls. There is no `accept4` and no flag upon `socket` that closes a descriptor across a spawn, so close-on-exec is set afterwards with `fcntl` | +| `openkal.datagram` | the same calls, with `SOCK_DGRAM` | +| `openkal.timeout` | `poll`, whose bound is stated in milliseconds. `ppoll` does not exist here, so a millisecond is the granularity this implementation reports — what the environment can distinguish rather than what would be convenient | +| `openkal.space` | the duplication primitive `openkal.process` was already built on. ⚠️ The duplicate is distinguished by the call's **second** return value: both images receive a process identifier in the first, so an implementation testing that one against zero would decide that neither image was the duplicate | +| `openkal.exec` | a mapping obtained writable and made executable afterwards, which is the only order this system permits. ⚠️ The instruction-cache maintenance is **not** performed here: `__builtin___clear_cache` becomes a call to `___clear_cache` in the compiler's support library on `arm64`, and this implementation is reachable from a program that carries no other runtime. The specification places that maintenance upon the program | + +**⚠️ Clause 6.5 names this system and `openkal.exec` in terms**, and says +availability may be settled by how the artifact is *produced* — a signed +declaration applied after the link. That clause is about memory which is +writable and executable **at the same time**, which is not what this interface +offers: a region here is writable, then published, then executable, and never +both. The conformance suite reserves a region, writes an instruction sequence +into it, publishes it, **calls it**, and compares what it returned — so the +reading is settled by the system rather than by this paragraph. If that +observation stops holding, the remedy is clause 6.5's: a feature of this +package, provided at dependency resolution. + The package exports no module: the interface belongs to the specification, and this package supplies definitions. @@ -118,7 +140,7 @@ rather than the specification. ```bash git clone https://github.com/mcpplibs/openkal .spec -bash .spec/tools/run-conformance.sh openkal-macos . full +bash .spec/tools/run-conformance.sh openkal-macos . full,optional ``` ## Architectures diff --git a/mcpp.toml b/mcpp.toml index 38d705a..fc8a9c1 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.4.0" +version = "0.5.0" description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used." license = "Apache-2.0" diff --git a/src/datagram.cpp b/src/datagram.cpp new file mode 100644 index 0000000..1074ebc --- /dev/null +++ b/src/datagram.cpp @@ -0,0 +1,153 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include + +// openkal.datagram upon this kernel's socket calls. +// +// A DATAGRAM IS NOT PACKED AS A kal_stream, and the handle type is its own for +// that reason: kal_stream_read reports a count and not a boundary, so reading a +// datagram through it would lose the property that distinguishes this interface. +// The packing is the same, the type is not, and the type is what prevents the +// mistake. + +namespace { + +int fd_of(kal_datagram d) { return okm::unpack(d.h); } + +} // namespace + +extern "C" { + +int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) { + if (out == nullptr) return kal_err_invalid; + + // A null local endpoint asks for one that may send and whose receiving + // address is unspecified. IPv4 is chosen for it, because a family must be + // named at the point the socket is made and this is the one every + // environment that has a network at all provides. + okm_long family = okm::af_inet; + if (local != nullptr) { + family = okm::family_of(*local); + if (family < 0) return kal_err_invalid; + } + + const okm_long fd = okm::sys(okm::nr_socket, family, okm::sock_dgram, + okm::ipproto_udp); + if (okm::failed(fd)) return okm::translate(fd); + okm::sys(okm::nr_fcntl, fd, okm::f_setfd, okm::fd_cloexec); + + if (local != nullptr) { + okm::ksockaddr_storage ss{}; + okm_u32 len = 0; + if (const int rc = okm::to_kernel(*local, ss, len); rc != kal_ok) { + okm::sys(okm::nr_close, fd); + return rc; + } + if (const okm_long r = okm::sys(okm::nr_bind, fd, + reinterpret_cast(&ss), + static_cast(len)); + okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + } + + out->h = okm::pack(static_cast(fd)); + if (out->h == 0) { okm::sys(okm::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_datagram_local(kal_datagram d, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = fd_of(d); + if (fd < 0) return kal_err_invalid; + + okm::ksockaddr_storage ss{}; + okm_u32 len = static_cast(sizeof ss); + const okm_long r = okm::sys(okm::nr_getsockname, fd, + reinterpret_cast(&ss), + reinterpret_cast(&len)); + if (okm::failed(r)) return okm::translate(r); + return okm::from_kernel(ss, *out); +} + +kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, + const kal_endpoint* to) { + const int fd = fd_of(d); + if (fd < 0 || to == nullptr) return { 0, kal_err_invalid }; + + okm::ksockaddr_storage ss{}; + okm_u32 addrlen = 0; + if (const int rc = okm::to_kernel(*to, ss, addrlen); rc != kal_ok) + return { 0, rc }; + + for (;;) { + const okm_long r = okm::sys(okm::nr_sendto, fd, + reinterpret_cast(buf), + static_cast(len), 0, + reinterpret_cast(&ss), + static_cast(addrlen)); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return { 0, okm::translate(r) }; + + // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface + // states. The kernel reports a count anyway; a count short of the length + // would mean the medium had split the message, which for a datagram + // socket it does not do. Reporting the short count as success would give + // a caller a partial send this interface says cannot occur, so it is + // reported as a failure of the medium instead. + const kal_uintptr n = static_cast(r); + return { n, n == len ? kal_ok : kal_err_io }; + } +} + +kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from) { + const int fd = fd_of(d); + if (fd < 0) return { 0, kal_err_invalid }; + + okm::ksockaddr_storage ss{}; + okm_u32 addrlen = static_cast(sizeof ss); + + for (;;) { + const okm_long r = okm::sys(okm::nr_recvfrom, fd, + reinterpret_cast(buf), + static_cast(len), 0, + reinterpret_cast(&ss), + reinterpret_cast(&addrlen)); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return { 0, okm::translate(r) }; + + // THE COUNT REPORTED IS WHAT WAS PLACED IN THE BUFFER, not what was + // sent. Without MSG_TRUNC the kernel already reports the former, which + // is what this interface requires: a caller that trusted the larger + // number would read beyond its own buffer. + if (from != nullptr) { + // A sender whose family this implementation does not know leaves the + // endpoint zeroed rather than partly filled. The transfer still + // happened and is reported; what is unknown is who sent it. + if (okm::from_kernel(ss, *from) != kal_ok) { + for (auto& b : from->addr) b = 0; + from->addr_len = 0; + from->port = 0; + } + } + return { static_cast(r), kal_ok }; + } +} + +void kal_datagram_close(kal_datagram d) { + const int fd = fd_of(d); + if (fd < 0) return; + okm::sys(okm::nr_close, fd); + okm::retire(d.h); +} + +// Broadcast is not claimed. The kernel provides it only after SO_BROADCAST has +// been set, and this interface has no operation that would set it; a word +// claiming a facility no operation reaches is the disagreement clause 6.2 exists +// to prevent. +const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6; + +} // extern "C" diff --git a/src/endpoint.h b/src/endpoint.h new file mode 100644 index 0000000..b7ca840 --- /dev/null +++ b/src/endpoint.h @@ -0,0 +1,120 @@ +// Conversion between kal_endpoint and this kernel's socket address structures. +// +// SHARED BY openkal.net AND openkal.datagram BECAUSE THE TYPE IS. Either +// interface may be provided without the other, so the conversion belongs to +// neither; writing it twice would be one decision stated in two places, and the +// two would eventually disagree about which lengths are accepted. +#pragma once +#include "sys.h" +#include + +namespace okm { + +// The port is carried in host order by kal_endpoint and in network order by the +// kernel. The conversion is written out rather than taken from a C library's +// htons, for the reason the head of sys.h gives. +inline unsigned short to_net_port(kal_u32 port) { + const unsigned short p = static_cast(port & 0xffffu); + return static_cast((p << 8) | (p >> 8)); +} +inline kal_u32 from_net_port(unsigned short net) { + return static_cast((net << 8) | (net >> 8)) & 0xffffu; +} + +// Fills a kernel address from an endpoint, and reports its length. +// +// ⚠️ THE LENGTH IS WRITTEN INTO THE STRUCTURE AS WELL AS RETURNED, because this +// kernel's layout carries one and the other kernel's does not. A structure left +// with a zero there is accepted by some calls and not by others, which is the +// worst of the three possible behaviours. +// +// A LENGTH THIS IMPLEMENTATION DOES NOT KNOW IS REFUSED RATHER THAN READ AS ONE +// IT DOES. The specification defines the set of lengths and allows it to grow; +// an implementation that ignored the field would misread every address a later +// revision defines, and would do so silently. +inline int to_kernel(const kal_endpoint& ep, ksockaddr_storage& out, okm_u32& len) { + fill(&out, 0, sizeof out); + + if (ep.addr_len == 4) { + auto* v4 = reinterpret_cast(&out); + v4->len = sizeof(ksockaddr_in); + v4->family = static_cast(af_inet); + v4->port = to_net_port(ep.port); + okm_u32 a = 0; + for (int i = 0; i < 4; ++i) + a |= static_cast(ep.addr[i]) << (i * 8); // already network order + v4->addr = a; + len = static_cast(sizeof(ksockaddr_in)); + return kal_ok; + } + + // Sixteen bytes is an address; twenty is an address followed by a scope + // identifier, which is carried in the four bytes after it. + if (ep.addr_len == 16 || ep.addr_len == 20) { + auto* v6 = reinterpret_cast(&out); + v6->len = sizeof(ksockaddr_in6); + v6->family = static_cast(af_inet6); + v6->port = to_net_port(ep.port); + v6->flowinfo = 0; + for (int i = 0; i < 16; ++i) v6->addr[i] = ep.addr[i]; + okm_u32 scope = 0; + if (ep.addr_len == 20) + for (int i = 0; i < 4; ++i) + scope |= static_cast(ep.addr[16 + i]) << (i * 8); + v6->scope_id = scope; + len = static_cast(sizeof(ksockaddr_in6)); + return kal_ok; + } + + return kal_err_invalid; +} + +// Fills an endpoint from a kernel address. A family this implementation does +// not know leaves the endpoint zeroed and reports it, for the same reason. +inline int from_kernel(const ksockaddr_storage& in, kal_endpoint& out) { + for (auto& b : out.addr) b = 0; + out.addr_len = 0; + out.port = 0; + + const unsigned char family = in.pad[1]; // the second byte, per the layout + + if (family == af_inet) { + const auto* v4 = reinterpret_cast(&in); + const okm_u32 a = v4->addr; + for (int i = 0; i < 4; ++i) + out.addr[i] = static_cast((a >> (i * 8)) & 0xffu); + out.addr_len = 4; + out.port = from_net_port(v4->port); + return kal_ok; + } + + if (family == af_inet6) { + const auto* v6 = reinterpret_cast(&in); + for (int i = 0; i < 16; ++i) out.addr[i] = v6->addr[i]; + // A zero scope identifier is reported as the shorter form. The two + // lengths denote the same address when the scope is zero, and reporting + // the shorter one keeps an address that came in as sixteen bytes going + // back out as sixteen. + if (v6->scope_id == 0) { + out.addr_len = 16; + } else { + for (int i = 0; i < 4; ++i) + out.addr[16 + i] = static_cast((v6->scope_id >> (i * 8)) & 0xffu); + out.addr_len = 20; + } + out.port = from_net_port(v6->port); + return kal_ok; + } + + return kal_err_invalid; +} + +// Which socket family an endpoint asks for, or -1 for a length that is not one +// of the defined ones. +inline okm_long family_of(const kal_endpoint& ep) { + if (ep.addr_len == 4) return af_inet; + if (ep.addr_len == 16 || ep.addr_len == 20) return af_inet6; + return -1; +} + +} // namespace okm diff --git a/src/exec.cpp b/src/exec.cpp new file mode 100644 index 0000000..34625d7 --- /dev/null +++ b/src/exec.cpp @@ -0,0 +1,115 @@ +#include "sys.h" +#include + +// openkal.exec on this system. +// +// A mapping obtained writable and made executable afterwards, which is the +// order the interface requires and the only order this system permits: a +// mapping that is writable and executable at once is refused here, and the +// interface states the narrower contract for exactly that reason. +// +// ⚠️⚠️ CLAUSE 6.5 NAMES THIS SYSTEM AND THIS INTERFACE IN TERMS, and what it +// says is that availability may be settled by how the artifact is PRODUCED --- +// a signed declaration applied after the link, by whoever produces the +// artifact. That would make the interface a feature of this package rather than +// a part of it. +// +// ⭐ THIS IMPLEMENTATION PROVIDES IT UNCONDITIONALLY, AND THE READING IS A +// MEASUREMENT RATHER THAN A CHOICE. The declaration clause 6.5 describes is +// what a program needs to obtain memory that is writable and executable AT THE +// SAME TIME --- `MAP_JIT' and the pair of calls that flip one region between +// the two. It is not what a program needs to obtain memory that is writable, +// then made executable and no longer writable, which is the whole of what this +// interface offers. tests/conformance_exec.cpp is where that distinction stops +// being an argument: it reserves a region, writes an instruction sequence into +// it, publishes it, CALLS it, and compares what it returned. +// +// ⇒ If that test fails on this system, the reading above is wrong and the +// remedy is clause 6.5's: a feature of this package, provided at dependency +// resolution, together with whatever produces the artifact. The test is here so +// that the question is answered by the system rather than by this comment. +// +// ⚠️⚠️ THE INSTRUCTION CACHE IS NOT INVALIDATED HERE, AND ONE VERSION OF THIS +// FILE DID INVALIDATE IT. THE MEASUREMENT IS WHY IT DOES NOT. +// +// A processor with separate caches for data and instructions has just had bytes +// written through the data path that it is about to fetch through the +// instruction path, and nothing in `mprotect' makes the second path see the +// first's writes. So `__builtin___clear_cache' looked like the right thing to +// add, on the reading that it expands to nothing on x86_64 and to the +// maintenance sequence INLINE on aarch64. +// +// ⚠️ The second half of that reading was false, and this package's own +// independence check said so within the hour: +// +// target/aarch64-macos/…/obj/exec.o references a symbol it must not: +// ___clear_cache +// +// The builtin becomes a CALL into the compiler's support library on this +// architecture, exactly as it does on riscv64. This implementation is reachable +// from a program that carries no other runtime and the check exists to keep it +// that way, so acquiring that dependency is not available here. +// +// ⭐ THE SPECIFICATION PLACES THE MAINTENANCE UPON THE PROGRAM in any case --- the +// conformance suite performs it itself and says why: the program is the party +// that knows which bytes it wrote. So nothing is lost by not doing it, and what +// the three implementations now share is a rule rather than an accident: +// +// an implementation performs the maintenance where its environment offers +// it as a CALL of the environment's own (openkal-windows has +// `FlushInstructionCache'), and does not where the only means is a compiler +// builtin that becomes a dependency upon the compiler's support library. +// +// openkal-linux/src/exec.cpp records the same rule from the other side. + +namespace { + +constexpr okm_uptr kPage = 4096; + +// ⚠️ THE PAGE IS 16384 BYTES ON ONE OF THIS SYSTEM'S TWO ARCHITECTURES. Rounding +// to the smaller number is still correct --- the kernel rounds up to its own +// granularity, and a region reserved as 4096 occupies a whole page of whatever +// size --- but a caller freeing with the size it reserved must reach the same +// number, which it does because both go through the same rounding. +okm_uptr round_up(okm_uptr n, okm_uptr to) { return (n + to - 1) & ~(to - 1); } + +} // namespace + +extern "C" { + +void* kal_exec_alloc(kal_uintptr size) { + if (size == 0) return nullptr; + const okm_uptr bytes = round_up(static_cast(size), kPage); + const okm_long r = okm::sys(okm::nr_mmap, 0, static_cast(bytes), + okm::prot_read | okm::prot_write, + okm::map_private | okm::map_anon, -1, 0); + if (okm::failed(r)) return nullptr; + return reinterpret_cast(r); +} + +int kal_exec_publish(void* p, kal_uintptr size) { + if (p == nullptr || size == 0) return kal_err_invalid; + const okm_uptr bytes = round_up(static_cast(size), kPage); + const okm_long r = okm::sys(okm::nr_mprotect, reinterpret_cast(p), + static_cast(bytes), + okm::prot_read | okm::prot_exec); + if (okm::failed(r)) return okm::translate(r); + return kal_ok; +} + +void kal_exec_free(void* p, kal_uintptr size) { + if (p == nullptr || size == 0) return; + const okm_uptr bytes = round_up(static_cast(size), kPage); + okm::sys(okm::nr_munmap, reinterpret_cast(p), + static_cast(bytes)); +} + +// A published region may NOT be reserved for writing again on this system, and +// the position is withheld accordingly. Asking this kernel to make an executable +// mapping writable is the case it refuses, which is the whole reason the +// interface separates the two states; a caller that must change published bytes +// reserves a second region and abandons the first, which is what the header +// says a zero here means. +const kal_uintptr kal_exec_props = 0; + +} // extern "C" diff --git a/src/net.cpp b/src/net.cpp new file mode 100644 index 0000000..c1ed06f --- /dev/null +++ b/src/net.cpp @@ -0,0 +1,202 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include + +// openkal.net upon this kernel's socket calls. +// +// A CONNECTION IS AN OWNED HANDLE AND THE STREAM IS BORROWED FROM IT, exactly as +// kal_file and kal_fs_stream are here. The owned handle carries a generation so +// that a released one stops being valid, which clause 7.2 requires; the stream +// it yields is the bare descriptor, because that is what openkal.stream's +// transfer operations take. +// +// ⚠️ TWO DIFFERENCES FROM THE OTHER KERNEL, BOTH IN THE SHAPE OF THE CALLS +// RATHER THAN IN WHAT THEY DO. +// +// There is no `accept4' and no flag upon `socket' that closes a descriptor +// across a spawn, so close-on-exec is set afterwards with `fcntl'. The two +// steps are not equivalent to one under a concurrent spawn, and process.cpp +// already records that this implementation states the difference rather than +// concealing it. +// +// A socket address carries its own length in its first byte. endpoint.h +// writes it; a structure copied from the other kernel would put the family +// where this one reads a length. + +namespace { + +int fd_of(kal_net_conn c) { return okm::unpack(c.h); } +int fd_of(kal_net_listener l) { return okm::unpack(l.h); } + +void close_on_exec(okm_long fd) { + okm::sys(okm::nr_fcntl, fd, okm::f_setfd, okm::fd_cloexec); +} + +int report_address(okm_long call, int fd, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + if (fd < 0) return kal_err_invalid; + okm::ksockaddr_storage ss{}; + okm_u32 len = static_cast(sizeof ss); + const okm_long r = okm::sys(call, fd, reinterpret_cast(&ss), + reinterpret_cast(&len)); + if (okm::failed(r)) return okm::translate(r); + return okm::from_kernel(ss, *out); +} + +} // namespace + +extern "C" { + +int kal_net_connect(const kal_endpoint* to, kal_net_conn* out) { + if (to == nullptr || out == nullptr) return kal_err_invalid; + const okm_long family = okm::family_of(*to); + if (family < 0) return kal_err_invalid; + + okm::ksockaddr_storage ss{}; + okm_u32 len = 0; + if (const int rc = okm::to_kernel(*to, ss, len); rc != kal_ok) return rc; + + const okm_long fd = okm::sys(okm::nr_socket, family, okm::sock_stream, + okm::ipproto_tcp); + if (okm::failed(fd)) return okm::translate(fd); + close_on_exec(fd); + + for (;;) { + const okm_long r = okm::sys(okm::nr_connect, fd, + reinterpret_cast(&ss), + static_cast(len)); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + break; + } + + out->h = okm::pack(static_cast(fd)); + if (out->h == 0) { okm::sys(okm::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_listen(const kal_endpoint* local, kal_net_listener* out) { + if (local == nullptr || out == nullptr) return kal_err_invalid; + const okm_long family = okm::family_of(*local); + if (family < 0) return kal_err_invalid; + + okm::ksockaddr_storage ss{}; + okm_u32 len = 0; + if (const int rc = okm::to_kernel(*local, ss, len); rc != kal_ok) return rc; + + const okm_long fd = okm::sys(okm::nr_socket, family, okm::sock_stream, + okm::ipproto_tcp); + if (okm::failed(fd)) return okm::translate(fd); + close_on_exec(fd); + + // SO_REUSEADDR, because a listener whose predecessor is in the kernel's + // lingering state would otherwise be refused for a reason that has nothing + // to do with the caller. A program restarted within the linger interval is + // the ordinary case, not an unusual one. + { + const int on = 1; + okm::sys(okm::nr_setsockopt, fd, okm::sol_socket, okm::so_reuseaddr, + reinterpret_cast(&on), + static_cast(sizeof on)); + } + + if (const okm_long r = okm::sys(okm::nr_bind, fd, + reinterpret_cast(&ss), + static_cast(len)); + okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + + // The backlog the kernel is asked for. A number rather than a name, because + // this interface does not expose one and a caller has no way to state it. + if (const okm_long r = okm::sys(okm::nr_listen, fd, 128); okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + + out->h = okm::pack(static_cast(fd)); + if (out->h == 0) { okm::sys(okm::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_accept(kal_net_listener l, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = fd_of(l); + if (fd < 0) return kal_err_invalid; + + for (;;) { + const okm_long r = okm::sys(okm::nr_accept, fd, 0, 0); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return okm::translate(r); + close_on_exec(r); + out->h = okm::pack(static_cast(r)); + if (out->h == 0) { okm::sys(okm::nr_close, r); return kal_err_no_memory; } + return kal_ok; + } +} + +kal_uintptr kal_net_stream(kal_net_conn c) { + // The bare descriptor, for the reason kal_fs_stream gives: openkal.stream's + // operations take whatever the environment's transfer calls take, and a + // packed word is not that. + const int fd = fd_of(c); + return fd < 0 ? 0u : static_cast(fd); +} + +int kal_net_peer(kal_net_conn c, kal_endpoint* out) { + return report_address(okm::nr_getpeername, fd_of(c), out); +} + +int kal_net_local(kal_net_conn c, kal_endpoint* out) { + return report_address(okm::nr_getsockname, fd_of(c), out); +} + +int kal_net_listener_local(kal_net_listener l, kal_endpoint* out) { + return report_address(okm::nr_getsockname, fd_of(l), out); +} + +int kal_net_shutdown(kal_net_conn c, int direction) { + const int fd = fd_of(c); + if (fd < 0) return kal_err_invalid; + + // The kernel numbers the directions from zero and this interface from one, + // so the mapping is written out rather than arithmetic upon the argument. A + // direction this interface does not define is refused rather than passed + // through, because the kernel would read an unknown number as SHUT_RD. + okm_long how; + switch (direction) { + case KAL_SHUT_READ: how = 0; break; + case KAL_SHUT_WRITE: how = 1; break; + case KAL_SHUT_BOTH: how = 2; break; + default: return kal_err_invalid; + } + + const okm_long r = okm::sys(okm::nr_shutdown, fd, how); + if (okm::failed(r)) return okm::translate(r); + return kal_ok; +} + +void kal_net_close(kal_net_conn c) { + const int fd = fd_of(c); + if (fd < 0) return; + okm::sys(okm::nr_close, fd); + okm::retire(c.h); +} + +void kal_net_close_listener(kal_net_listener l) { + const int fd = fd_of(l); + if (fd < 0) return; + okm::sys(okm::nr_close, fd); + okm::retire(l.h); +} + +// Both positions hold on this kernel: it speaks IPv6 and its `shutdown' ends +// transfer in one direction while the other continues. +const kal_uintptr kal_net_props = KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; + +} // extern "C" diff --git a/src/space.cpp b/src/space.cpp new file mode 100644 index 0000000..1204033 --- /dev/null +++ b/src/space.cpp @@ -0,0 +1,68 @@ +#include "sys.h" +#include + +// openkal.space upon this kernel's duplication primitive. +// +// THE PRIMITIVE IS ALREADY IN src/sys.h AND WAS ALREADY IN USE: openkal.process +// starts a program by duplicating this one and replacing the duplicate, so the +// call, its two return values and the reason the second one matters were +// written down before this interface existed. What is added here is the other +// use of the same primitive --- the one where the duplicate is not replaced. +// +// ⚠️ THE DUPLICATE IS DISTINGUISHED BY THE SECOND VALUE THE CALL RETURNS AND +// NOT BY THE FIRST. Both images receive a process identifier in the first +// register on this system: the original receives the duplicate's and the +// duplicate receives the ORIGINAL's, so an implementation that tested the first +// against zero --- which is what the other kernel's convention would suggest --- +// would decide that neither image was the duplicate. src/sys.h records the +// measurement beside the call. + +extern "C" { + +int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, + kal_process* out) { + if (entry == nullptr || out == nullptr) return kal_err_invalid; + + // THE STACK ARGUMENT IS ACCEPTED AND IGNORED, AND THE HEADER STATES THAT AS + // ONE OF THE TWO PERMITTED BEHAVIOURS. This kernel's primitive gives the + // started context a copy of the caller's own stack, so there is nothing for + // a caller-supplied one to be used for; a caller cannot observe which of the + // two occurred and has no decision resting upon it. + (void)stack_top; + + bool is_duplicate = false; + const okm_long child = okm::duplicate(is_duplicate); + if (okm::failed(child)) return okm::translate(child); + + if (is_duplicate) { + entry(arg); + // THE ENTRY IS NOT REQUIRED TO RETURN, AND IF IT DOES THE CONTEXT ENDS. + // + // Returning from here would return into the duplication's caller in the + // copied space, which is the whole program running a second time from + // the middle of this function. Ending the context is the only defined + // thing to do, and the status says the entry returned rather than + // choosing one. + for (;;) okm::sys(okm::nr_exit, 0); + } + + *out = kal_process{ static_cast(child) }; + return kal_ok; +} + +// Both positions hold on this kernel. +// +// The handles accompany the memory: the duplicate receives a copy of the +// descriptor table, so every handle the caller holds is open in the copy at the +// same number, and the packing in handle.h recovers the same descriptor from the +// same word. +// +// The copy is deferred: this kernel maps the pages copy-on-write, so a store to +// copied memory can fail with the machine out of memory after this call has +// already reported success. An implementation cannot undefer that, and stating +// it is what lets a program that cannot tolerate it know which environment it +// is in. +const kal_uintptr kal_space_props = + KAL_SPACE_PROP_CLONE_HANDLES | KAL_SPACE_PROP_DEFERRED_COPY; + +} // extern "C" diff --git a/src/sys.h b/src/sys.h index ffee25f..e248d4a 100644 --- a/src/sys.h +++ b/src/sys.h @@ -181,8 +181,68 @@ enum : okm_long { nr_fstatat64 = 470, nr_unlinkat = 472, nr_readlinkat = 473, nr_mkdirat = 475, nr_ulock_wait = 515, nr_ulock_wake = 516, + + // The socket calls, and the one that reports readiness. + // + // ⚠️ THEY ARE THE ORIGINAL BSD NUMBERS AND THEY ARE LOW, which is worth + // stating because the numbering here is not the other kernel's in any + // respect: `accept' is 30 there and 288 here, and a table copied from the + // wrong system produces a signal named "bad system call" rather than a + // failed call. .github/workflows/numbers.yml reads them from the system's + // own header on both architectures. + // + // ⭐ THERE IS NO `accept4' AND NO `pipe2' ON THIS SYSTEM, so close-on-exec + // is set after the fact with `fcntl'. process.cpp already records what that + // costs and why a program using these operations from one context is not + // affected by it. + nr_recvfrom = 29, nr_accept = 30, nr_getpeername = 31, nr_getsockname = 32, + nr_select = 93, nr_socket = 97, nr_connect = 98, + nr_bind = 104, nr_setsockopt = 105, nr_listen = 106, + nr_getsockopt = 118, nr_sendto = 133, nr_shutdown = 134, + nr_poll = 230, +}; + +// --- the network's own constants --------------------------------------------- +// +// ⚠️ TWO OF THESE DIFFER FROM THE OTHER KERNEL'S AND WOULD NOT ANNOUNCE IT. +// `AF_INET6' is 30 here and 10 there, and `SOL_SOCKET' is 0xffff here and 1 +// there. A value taken from the wrong system produces a call that fails with +// an ordinary error, which reads as a defect in the caller. +enum : okm_long { + af_inet = 2, af_inet6 = 30, + sock_stream = 1, sock_dgram = 2, + ipproto_tcp = 6, ipproto_udp = 17, + sol_socket = 0xffff, so_reuseaddr = 0x0004, + f_setfd = 2, fd_cloexec = 1, + poll_in = 0x0001, poll_out = 0x0004, + wnohang = 1, }; +// This kernel's socket addresses. ⚠️ THE FIRST BYTE IS A LENGTH, which the +// other kernel's layout does not have: there the family occupies two bytes and +// here it occupies the second byte alone. A structure copied from that system +// puts the family where this one reads a length. +struct ksockaddr_in { + unsigned char len; + unsigned char family; + unsigned short port; // network order + okm_u32 addr; // network order + unsigned char zero[8]; +}; + +struct ksockaddr_in6 { + unsigned char len; + unsigned char family; + unsigned short port; // network order + okm_u32 flowinfo; + unsigned char addr[16]; + okm_u32 scope_id; +}; + +struct ksockaddr_storage { unsigned char pad[128]; }; + +struct kpollfd { int fd; short events; short revents; }; + // --- error values, as this kernel returns them ------------------------------- enum : int { e_perm = 1, e_noent = 2, e_intr = 4, e_io = 5, e_badf = 9, e_child = 10, @@ -203,7 +263,7 @@ enum : okm_long { o_creat = 0x0200, o_excl = 0x0800, o_trunc = 0x0400, o_append = 0x0008, o_directory = 0x100000, o_cloexec = 0x1000000, o_nofollow = 0x0100, at_fdcwd = -2, at_removedir = 0x0080, at_symlink_nofollow = 0x0020, - prot_read = 1, prot_write = 2, + prot_read = 1, prot_write = 2, prot_exec = 4, map_private = 2, map_anon = 0x1000, }; diff --git a/src/timeout.cpp b/src/timeout.cpp new file mode 100644 index 0000000..fec8b7b --- /dev/null +++ b/src/timeout.cpp @@ -0,0 +1,165 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include +#include + +// openkal.timeout upon poll(2) and wait4(2). +// +// THE BOUND IS APPLIED BEFORE THE OPERATION AND NOT DURING IT. `poll' reports +// whether a descriptor would transfer without blocking, so a bounded read is a +// bounded wait for readiness followed by the ordinary read. This is what the +// environment already does at the point of the call, which is why clause 6.3 +// records readiness notification as the alternative that was NOT adopted: an +// interface reporting readiness would oblige an implementation to maintain a set +// and a context of its own, and this one obliges it to maintain nothing. +// +// The bound is therefore upon the WAIT and not upon the transfer. A read that +// becomes ready within the bound and then transfers slowly is not interrupted, +// which is the behaviour every environment's own bounded read has. +// +// ⚠️ THIS KERNEL'S `poll' STATES ITS BOUND IN MILLISECONDS, and `ppoll' --- the +// call the other kernel uses, which takes nanoseconds --- does not exist here. +// The granularity this interface reports is therefore a millisecond, which is +// what the environment can distinguish rather than what would be convenient. + +namespace { + +// A duration of zero denotes no bound, which is the convention kal_task_wait +// establishes. `poll' expresses that with a negative number. +// +// ⚠️ A BOUND SHORTER THAN A MILLISECOND ROUNDS UP TO ONE AND NOT DOWN TO NONE. +// Rounding down would turn a wait into a poll, and the header is explicit: a +// caller that asks for less is not refused and does not get less. +int bound_ms(kal_u64 ns) { + if (ns == 0) return -1; + okm_u64 ms = ns / 1000000ull; + if (ms == 0) ms = 1; + if (ms > 0x7fffffffull) ms = 0x7fffffffull; + return static_cast(ms); +} + +// Waits for one descriptor. Reports kal_ok when it is ready, kal_err_again when +// the bound expired, and a translated error otherwise. +int await(int fd, short events, kal_u64 ns) { + okm::kpollfd p{ fd, events, 0 }; + const int ms = bound_ms(ns); + + const okm_long r = okm::sys(okm::nr_poll, reinterpret_cast(&p), 1, ms); + // AN INTERRUPTED WAIT IS NOT RETRIED WITH THE WHOLE BOUND AGAIN. + // + // Retrying with the original duration would make the bound restart at every + // signal, so a program on a system that delivers them regularly would wait + // without end while appearing to be bounded. The call leaves nothing behind + // to resume from, and the honest report is that the operation did not + // complete. + if (okm::interrupted(r)) return kal_err_again; + if (okm::failed(r)) return okm::translate(r); + if (r == 0) return kal_err_again; // the bound expired + return kal_ok; +} + +} // namespace + +extern "C" { + +kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { + // A transfer of zero bytes does not wait and is not bounded. Waiting first + // would turn a call that always succeeds into one that can expire. + if (len == 0) return { 0, kal_ok }; + + const int fd = okm::unpack(s.h); + // THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them + // as the descriptors themselves, so a word that does not unpack is taken to + // be one of those rather than being refused. + const int use = (fd >= 0) ? fd : static_cast(s.h); + + if (const int rc = await(use, static_cast(okm::poll_in), ns); rc != kal_ok) + return { 0, rc }; + return kal_stream_read(s, buf, len); +} + +kal_io_result kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { + if (len == 0) return { 0, kal_ok }; + + const int fd = okm::unpack(s.h); + const int use = (fd >= 0) ? fd : static_cast(s.h); + + if (const int rc = await(use, static_cast(okm::poll_out), ns); rc != kal_ok) + return { 0, rc }; + return kal_stream_write(s, buf, len); +} + +int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = okm::unpack(l.h); + if (fd < 0) return kal_err_invalid; + + if (const int rc = await(fd, static_cast(okm::poll_in), ns); rc != kal_ok) + return rc; + return kal_net_accept(l, out); +} + +kal_io_result kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from, kal_u64 ns) { + const int fd = okm::unpack(d.h); + if (fd < 0) return { 0, kal_err_invalid }; + + if (const int rc = await(fd, static_cast(okm::poll_in), ns); rc != kal_ok) + return { 0, rc }; + return kal_datagram_recv_from(d, buf, len, from); +} + +int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* terminated) { + if (p.h == 0) return kal_err_invalid; + + // WNOHANG AND A POLLING LOOP, BECAUSE THIS KERNEL HAS NO BOUNDED WAIT FOR A + // CHILD EITHER. `wait4' blocks or does not wait at all; there is no bound. + // + // The alternative is a handler for the signal a child's end raises, which + // is process-wide state: an implementation that installed one would take a + // facility away from the program above it. The loop is what the environment + // permits, and the interval is the granularity this interface reports so + // that the polling cost is stated rather than hidden. + constexpr okm_u64 interval_ns = 1000000ull; // one millisecond + okm_u64 waited = 0; + + for (;;) { + int st = 0; + const okm_long r = okm::sys(okm::nr_wait4, static_cast(p.h), + reinterpret_cast(&st), + okm::wnohang, 0); + if (okm::interrupted(r)) continue; + if (okm::failed(r)) return okm::translate(r); + + if (r != 0) { + const int signalled = st & 0x7f; + if (signalled == 0) { + if (status) *status = (st >> 8) & 0xff; + if (terminated) *terminated = 0; + } else { + if (status) *status = signalled; + if (terminated) *terminated = 1; + } + return kal_ok; + } + + if (ns != 0 && waited >= ns) return kal_err_again; + + // openkal.time's own suspension, rather than a second spelling of it. + // This implementation provides that interface and time.cpp records what + // this system's suspension is expressed with; reproducing it here would + // be the same decision written twice. + kal_time_sleep(interval_ns); + waited += interval_ns; + } +} + +// The bound this kernel distinguishes. `poll' takes milliseconds and there is +// no call here that takes less, so a millisecond is what an implementation can +// honestly report --- and it is also the interval the child-waiting loop above +// polls at, so a caller asking for less is not told a number one of the +// operations cannot meet. +const kal_uintptr kal_timeout_granularity_ns = 1000000u; + +} // extern "C"