Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down
21 changes: 20 additions & 1 deletion .github/workflows/numbers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
153 changes: 153 additions & 0 deletions src/datagram.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#include "sys.h"
#include "handle.h"
#include "endpoint.h"
#include <openkal/datagram.h>

// 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<okm_long>(&ss),
static_cast<okm_long>(len));
okm::failed(r)) {
okm::sys(okm::nr_close, fd);
return okm::translate(r);
}
}

out->h = okm::pack(static_cast<int>(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<okm_u32>(sizeof ss);
const okm_long r = okm::sys(okm::nr_getsockname, fd,
reinterpret_cast<okm_long>(&ss),
reinterpret_cast<okm_long>(&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<okm_long>(buf),
static_cast<okm_long>(len), 0,
reinterpret_cast<okm_long>(&ss),
static_cast<okm_long>(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<kal_uintptr>(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<okm_u32>(sizeof ss);

for (;;) {
const okm_long r = okm::sys(okm::nr_recvfrom, fd,
reinterpret_cast<okm_long>(buf),
static_cast<okm_long>(len), 0,
reinterpret_cast<okm_long>(&ss),
reinterpret_cast<okm_long>(&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<kal_uintptr>(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"
120 changes: 120 additions & 0 deletions src/endpoint.h
Original file line number Diff line number Diff line change
@@ -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 <openkal/types.h>

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<unsigned short>(port & 0xffffu);
return static_cast<unsigned short>((p << 8) | (p >> 8));
}
inline kal_u32 from_net_port(unsigned short net) {
return static_cast<kal_u32>((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<ksockaddr_in*>(&out);
v4->len = sizeof(ksockaddr_in);
v4->family = static_cast<unsigned char>(af_inet);
v4->port = to_net_port(ep.port);
okm_u32 a = 0;
for (int i = 0; i < 4; ++i)
a |= static_cast<okm_u32>(ep.addr[i]) << (i * 8); // already network order
v4->addr = a;
len = static_cast<okm_u32>(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<ksockaddr_in6*>(&out);
v6->len = sizeof(ksockaddr_in6);
v6->family = static_cast<unsigned char>(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<okm_u32>(ep.addr[16 + i]) << (i * 8);
v6->scope_id = scope;
len = static_cast<okm_u32>(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<const ksockaddr_in*>(&in);
const okm_u32 a = v4->addr;
for (int i = 0; i < 4; ++i)
out.addr[i] = static_cast<kal_u8>((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<const ksockaddr_in6*>(&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<kal_u8>((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
Loading
Loading