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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ written on the kernel's own calls.
openkal = "0.25.0"

[target.'cfg(os = "macos")'.dependencies]
openkal-macos = "0.23.0"
openkal-macos = "0.23.1"
```

Its purpose is as much to test the specification as to be used. A specification
Expand Down Expand Up @@ -183,6 +183,26 @@ same image parses as fast on Linux arm64. Since 0.17.1 it is the same kernel ide
this system's library keeps for each thread (`pthread_threadid_np`, a name no C
library in this ecosystem defines).

## What 0.23.1 corrects

**A handle is retired before its descriptor is closed.** A connection, a listener
and a datagram socket were closed first and retired second. The kernel gives a
closed number to the next open in any context, so a file opened by another context
between the two took the number with the generation the socket's handle still had,
and the retire then made the file's handle stale: its stream was answered as zero,
which is standard input, its reads returned nothing of the file, and its descriptor
was never closed (openkal-linux#53; this implementation has the same construction).
Every release is now one function, which retires and then closes.

**One release closes.** The generation is advanced by a compare-and-exchange from
the value the handle carries, so a second release of the same word, in the same
context or another, closes nothing. It is read and advanced through the atomic
operations.

**A released handle's stream is no stream.** `kal_fs_stream` and `kal_net_stream`
answer a handle that names no live descriptor with a word no descriptor has, and a
transfer upon it reports `kal_err_invalid`. They answered zero.

## What 0.23 adds (openkal 0.25)

**A second name for a file** (`kal_fs_hard_link`) is `linkat` with no flags, so a
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.23.0"
version = "0.23.1"
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
7 changes: 1 addition & 6 deletions src/datagram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,7 @@ kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
}
}

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);
}
void kal_datagram_close(kal_datagram d) { okm::release(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
Expand Down
10 changes: 3 additions & 7 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -517,19 +517,15 @@ void kal_fs_close_dir(kal_dir d) {
// directory whose enumerations are not this one's.
listings_end(fd);
okm::rights::forget(fd);
okm::retire(d.h); okm::sys(okm::nr_close, fd);
okm::release(d.h);
}

void kal_fs_close_file(kal_file f) {
const int fd = okm::unpack(f.h);
if (fd >= 0) { okm::retire(f.h); okm::sys(okm::nr_close, fd); }
}
void kal_fs_close_file(kal_file f) { okm::release(f.h); }

kal_uintptr kal_fs_max_name(void) { return okm::max_name; }

kal_stream kal_fs_stream(kal_file f) {
const int fd = okm::unpack(f.h);
return kal_stream{ fd < 0 ? 0u : static_cast<kal_uintptr>(fd) };
return kal_stream{ okm::stream_word(f.h) };
}

int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) {
Expand Down
47 changes: 43 additions & 4 deletions src/handle.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ inline unsigned* generations() {
return g;
}

// A generation is read by the context that opens a descriptor and advanced by
// the one that releases it, and the two are different contexts whenever a
// number is reused across them, so both go through the atomic operations.
inline unsigned generation(int fd) {
return __atomic_load_n(&generations()[fd], __ATOMIC_RELAXED);
}

inline okm_uptr pack(int fd) {
if (fd < 0 || fd >= kMaxDescriptor) return 0;
return (static_cast<okm_uptr>(generations()[fd]) << 32)
return (static_cast<okm_uptr>(generation(fd)) << 32)
| (static_cast<okm_uptr>(fd) + 1u);
}

Expand All @@ -39,13 +46,45 @@ inline okm_uptr pack(int fd) {
inline int unpack(okm_uptr h) {
const int fd = static_cast<int>(h & 0xffffffffu) - 1;
if (fd < 0 || fd >= kMaxDescriptor) return -1;
if (static_cast<unsigned>(h >> 32) != generations()[fd]) return -1;
if (static_cast<unsigned>(h >> 32) != generation(fd)) return -1;
return fd;
}

inline void retire(okm_uptr h) {
// RETIRE, THEN CLOSE. The kernel gives a closed number to the next open in any
// context. A handle retired after its descriptor was closed leaves a window in
// which another context opens the number, packs it with the generation the
// handle being released still has, and is then left with a handle this retire
// makes stale: its stream reads and writes nothing of its own, and its
// descriptor is never closed (openkal-linux#53, the same construction). Every
// release in this implementation is `release(h)`, which keeps the order in one
// place.
//
// One release of a handle advances its generation; a second release of the
// same word, in this context or another, finds it advanced and closes nothing,
// so it cannot close a descriptor opened since.
inline bool retire(okm_uptr h) {
const int fd = static_cast<int>(h & 0xffffffffu) - 1;
if (fd < 0 || fd >= kMaxDescriptor) return false;
unsigned held = static_cast<unsigned>(h >> 32);
return __atomic_compare_exchange_n(&generations()[fd], &held, held + 1u, false,
__ATOMIC_RELAXED, __ATOMIC_RELAXED);
}

inline void release(okm_uptr h) {
const int fd = static_cast<int>(h & 0xffffffffu) - 1;
if (retire(h)) sys(nr_close, fd);
}

// What a stream enquiry answers for a handle that does not name a live
// descriptor. A stream is a bare descriptor here (stream.cpp), so zero is
// standard input: a released handle answered with zero was read from and
// written to as standard input. No descriptor has this value, and every
// transfer upon it reports `kal_err_invalid`.
inline constexpr okm_uptr kNoStream = ~static_cast<okm_uptr>(0);

inline okm_uptr stream_word(okm_uptr h) {
const int fd = unpack(h);
if (fd >= 0) ++generations()[fd];
return fd < 0 ? kNoStream : static_cast<okm_uptr>(fd);
}

} // namespace okm
17 changes: 3 additions & 14 deletions src/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,7 @@ kal_stream 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 kal_stream{ fd < 0 ? 0u : static_cast<kal_uintptr>(fd) };
return kal_stream{ okm::stream_word(c.h) };
}

int kal_net_peer(kal_net_conn c, kal_endpoint* out) {
Expand Down Expand Up @@ -237,19 +236,9 @@ int kal_net_shutdown(kal_net_conn c, int direction) {
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(kal_net_conn c) { okm::release(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);
}
void kal_net_close_listener(kal_net_listener l) { okm::release(l.h); }

// Both positions hold on this kernel: it speaks IPv6 and its `shutdown' ends
// transfer in one direction while the other continues.
Expand Down
122 changes: 122 additions & 0 deletions tests/a_released_handle_is_stale_before_its_number_is_free.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// A handle is retired before its descriptor is closed (0.23.1; openkal-linux#53, the same construction).
//
// The kernel gives a closed number to the next open in any context. Until
// 0.23.1 a connection, a listener and a datagram socket were closed first and
// retired second: a file opened by another context between the two took the
// number with the generation the socket's handle still had, and the retire then
// made the file's handle stale. Its stream was answered as zero, standard
// input, so its reads returned nothing of the file and its descriptor was never
// closed. Measured by the report on four processors: about 150 such files a
// second.
//
// One context accepts connections and closes them, one connects and closes, and
// this one opens, reads and closes a file. Every read returns the file.
#include <atomic>
#include <chrono>
#include <cstring>
#include <thread>
import openkal.types;
import openkal.fs;
import openkal.net;
import openkal.stream;
import mcpplibs.mtest;

using namespace mcpplibs::mtest;

namespace {

// The working directory, which is the first directory the environment supplies.
kal_dir root() {
kal_dir d{}; char n[4096]; kal_uintptr l = 0;
return kal_fs_preopen(0, &d, n, sizeof n, &l) == kal_ok ? d : kal_dir{};
}

constexpr char kName[] = "okl_retire_probe.txt";
constexpr char kText[] = "the file's bytes"; // 16

} // namespace

UTest a_released_handle_is_stale_before_its_number_is_free{
"a_released_handle_is_stale_before_its_number_is_free", [] {
const kal_dir dir = root();
expect(fatal(dir.h != 0)) << "the environment supplies the working directory";
{
kal_file f{};
expect(fatal(kal_fs_open(dir, kName, sizeof kName - 1,
(kal::fs::open::write | kal::fs::open::create | kal::fs::open::truncate).bits,
&f) == kal_ok));
expect(fatal(kal_stream_write(kal_fs_stream(f), kText, 16) == 16));
kal_fs_close_file(f);
}

kal_endpoint any{};
any.addr[0] = 127; any.addr[3] = 1; any.addr_len = 4;
kal_net_listener lis{};
expect(fatal(kal_net_listen(&any, &lis) == kal_ok));
kal_endpoint at{};
expect(fatal(kal_net_listener_local(lis, &at) == kal_ok));

std::atomic<bool> stop{false};
std::atomic<long> served{0};
std::thread accepting([&] {
for (;;) {
kal_net_conn c{};
if (kal_net_accept(lis, &c) != kal_ok) break;
kal_stream_write(kal_net_stream(c), "okl", 3);
kal_net_close(c);
served.fetch_add(1, std::memory_order_relaxed);
if (stop.load(std::memory_order_relaxed)) break;
}
});
std::thread connecting([&] {
while (!stop.load(std::memory_order_relaxed)) {
kal_net_conn c{};
if (kal_net_connect(&at, &c) == kal_ok) kal_net_close(c);
}
// One more, so the accepting context leaves its wait and sees the word.
kal_net_conn last{};
if (kal_net_connect(&at, &last) == kal_ok) kal_net_close(last);
});

long opened = 0, stale = 0, short_reads = 0;
const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < until) {
for (int i = 0; i < 256; ++i) {
kal_file f{};
if (kal_fs_open(dir, kName, sizeof kName - 1, kal::fs::open::read.bits, &f) != kal_ok) continue;
++opened;
char buf[32]{};
const kal_intptr n = kal_stream_read(kal_fs_stream(f), buf, sizeof buf);
if (n < 0) ++stale;
else if (n != 16 || std::memcmp(buf, kText, 16) != 0) ++short_reads;
kal_fs_close_file(f);
}
}
stop.store(true, std::memory_order_relaxed);
connecting.join();
accepting.join();
kal_net_close_listener(lis);

expect(gt(opened, 0L));
expect(gt(served.load(), 0L)) << "no connection was closed beside the opens; the test measured nothing";
expect(eq(stale, 0L)) << "files whose handle was stale when its stream was taken, of " << opened;
expect(eq(short_reads, 0L)) << "files whose read was not the file, of " << opened;

// A released handle's stream is no stream: a transfer upon it is refused,
// and is not a transfer upon standard input.
kal_file f{};
expect(fatal(kal_fs_open(dir, kName, sizeof kName - 1, kal::fs::open::read.bits, &f) == kal_ok));
kal_fs_close_file(f);
char one = 0;
expect(eq(kal_stream_read(kal_fs_stream(f), &one, 1), static_cast<kal_intptr>(-kal_err_invalid)));
// And a second release of it closes nothing: the number may already be
// another's.
kal_file g{};
expect(fatal(kal_fs_open(dir, kName, sizeof kName - 1, kal::fs::open::read.bits, &g) == kal_ok));
kal_fs_close_file(f);
char buf[32]{};
expect(eq(kal_stream_read(kal_fs_stream(g), buf, sizeof buf), static_cast<kal_intptr>(16)));
kal_fs_close_file(g);

kal_fs_remove(dir, kName, sizeof kName - 1);
}};
Loading