diff --git a/README.md b/README.md
index 2b300f5..b4f4ea3 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,10 @@ for Linux, written on the kernel's own system-call interface.
```toml
[dependencies]
-openkal = "0.15.0"
+openkal = "0.16.0"
[target.'cfg(os = "linux")'.dependencies]
-openkal-linux = "0.16.1"
+openkal-linux = "0.17.0"
```
## Why it does not use a C library
@@ -45,6 +45,22 @@ second and third.
All eight. `tools/check-surface.sh --complete` in the specification package
compares the exported names against `SURFACE.txt`.
+## The stack a started context is given
+
+Where this implementation creates a context (the `standalone` arrangement) it
+maps the stack itself: 8 MiB unless more is asked for, which is the size the
+host's own C library gives a thread, and a 256 KiB inaccessible guard beneath
+it. The mapping is committed as it is touched, so the size costs address space
+rather than memory. The guard is larger than one page because a frame of 69,008
+bytes was measured in the C library above at `-O0`, and a frame larger than the
+guard steps over it.
+
+`kal_task_start_sized` (openkal 0.16) gives at least the size asked for and
+never less than the default, so a C library that asks for its own smaller
+default on every thread does not shrink what each one had. A request larger than
+the address space is `kal_err_no_memory`. Where the program carries a runtime of
+its own, the request is passed to that runtime.
+
## The region a context stands on
`kal_task_stack` reports the stack of the calling context, and this
diff --git a/mcpp.toml b/mcpp.toml
index 1b09646..d70fb84 100644
--- a/mcpp.toml
+++ b/mcpp.toml
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-linux"
-version = "0.16.1"
+version = "0.17.0"
description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one."
license = "Apache-2.0"
@@ -63,7 +63,7 @@ provides-interfaces = [
]
[dependencies]
-openkal = "0.15.0"
+openkal = "0.16.0"
# The package contributes definitions and no modules. The interface it
# implements is declared by the specification package, which this package
diff --git a/src/fs.cpp b/src/fs.cpp
index 01ebe71..ddc5086 100644
--- a/src/fs.cpp
+++ b/src/fs.cpp
@@ -254,11 +254,98 @@ kal_uintptr put_name(const char* src, kal_uintptr n,
// calling.
struct listing {
int fd;
+ int dir; // the descriptor of the directory it was begun upon
+ listing* next;
+ int busy; // a context is inside kal_fs_list_next with it
+ int doomed; // its directory was closed while it was busy
okl_uptr used;
okl_uptr pos;
char buf[8192];
};
+// THE ENUMERATIONS NOT YET READ TO THE END, BECAUSE CLOSING THE DIRECTORY MUST
+// END THEM. fs.h releases an iterator "by reading past the end or by closing
+// the directory that produced it", and the descriptor a listing holds is its
+// own, so closing the directory's descriptor does not reach it. Measured before
+// this list existed: a C library above that opened a directory larger than its
+// buffer, read one entry and closed it, 64 times, was left holding 64 more
+// descriptors.
+//
+// The lock spins rather than suspends, because suspending would make openkal.fs
+// depend upon openkal.task, as src/memory.cpp says of its own.
+listing* g_listings = nullptr;
+volatile int g_listings_held = 0;
+
+void listings_lock() {
+ while (__atomic_exchange_n(&g_listings_held, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield);
+}
+void listings_unlock() { __atomic_store_n(&g_listings_held, 0, __ATOMIC_RELEASE); }
+
+void listing_release(listing* s) {
+ okl::sys(okl::nr_close, s->fd);
+ kal_free(s, sizeof(listing), alignof(listing));
+}
+
+// Ends every listing begun upon `dir'. What is ended is unlinked under the lock
+// and released after it.
+//
+// A LISTING ANOTHER CONTEXT IS READING IS NOT RELEASED HERE. Clause 6.6 requires
+// that concurrent operations upon one handle not damage this implementation's
+// state, and releasing the buffer a getdents64 in another context is writing
+// into would do exactly that. Such a listing is marked, and the context reading
+// it releases it on the way out; it can no longer be found, so nothing reaches
+// it after that.
+void listings_end(int dir) {
+ listing* done = nullptr;
+ listings_lock();
+ for (listing** p = &g_listings; *p != nullptr;) {
+ listing* s = *p;
+ if (s->dir != dir || s->doomed) { p = &s->next; continue; }
+ if (s->busy) { s->doomed = 1; p = &s->next; continue; }
+ *p = s->next; s->next = done; done = s;
+ }
+ listings_unlock();
+ while (done != nullptr) { listing* s = done; done = s->next; listing_release(s); }
+}
+
+// Takes `s' for the calling context, or reports that it is not a listing of
+// `dir' that is still open.
+//
+// THE WORD IS FOUND RATHER THAN BELIEVED. An iterator is the caller's copy of an
+// address this implementation allocated, and closing its directory frees what
+// it named --- so a caller that kept it, or that raced the close, holds an
+// address that may by now be anything. Searching the open listings for it is
+// what lets kal_fs_list_next refuse such a word without reading through it. The
+// search is over enumerations not yet read to their end, which a program holds
+// a handful of.
+bool listing_take(listing* s, int dir) {
+ for (;;) {
+ listings_lock();
+ listing* p = g_listings;
+ while (p != nullptr && p != s) p = p->next;
+ if (p == nullptr || p->dir != dir || p->doomed) { listings_unlock(); return false; }
+ if (!p->busy) { p->busy = 1; listings_unlock(); return true; }
+ // Two contexts reading one enumeration take turns; the order in which
+ // they receive its entries is the unspecified order clause 6.6 permits.
+ listings_unlock();
+ okl::sys(okl::nr_sched_yield);
+ }
+}
+
+// Gives `s' back, and releases it if it reached its end or its directory was
+// closed while it was being read.
+void listing_give(listing* s, bool ended) {
+ listings_lock();
+ s->busy = 0;
+ const bool release = ended || s->doomed;
+ if (release) {
+ for (listing** p = &g_listings; *p != nullptr; p = &(*p)->next)
+ if (*p == s) { *p = s->next; break; }
+ }
+ listings_unlock();
+ if (release) listing_release(s);
+}
+
} // namespace
extern "C" {
@@ -312,7 +399,11 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len,
void kal_fs_close_dir(kal_dir d) {
const int fd = okl::unpack(d.h);
- if (fd >= 0) { okl::retire(d.h); okl::sys(okl::nr_close, fd); }
+ if (fd < 0) return;
+ // Before the close, because after it the number may already name another
+ // directory whose enumerations are not this one's.
+ listings_end(fd);
+ okl::retire(d.h); okl::sys(okl::nr_close, fd);
}
void kal_fs_close_file(kal_file f) {
@@ -515,16 +606,23 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) {
if (okl::failed(own)) return okl::translate(own);
auto* s = static_cast
(kal_alloc(sizeof(listing), alignof(listing)));
if (s == nullptr) { okl::sys(okl::nr_close, own); return kal_err_no_memory; }
- s->fd = static_cast(own); s->used = 0; s->pos = 0;
+ s->fd = static_cast(own); s->dir = fd; s->busy = 0; s->doomed = 0;
+ s->used = 0; s->pos = 0;
+ listings_lock(); s->next = g_listings; g_listings = s; listings_unlock();
*iter = reinterpret_cast(s);
return kal_ok;
}
-int kal_fs_list_next(kal_dir, kal_uintptr* iter,
+int kal_fs_list_next(kal_dir d, kal_uintptr* iter,
char* name_out, kal_uintptr name_cap,
kal_uintptr* name_len, int* kind) {
- if (iter == nullptr || *iter == 0) return kal_err_invalid;
+ // A CLOSED DIRECTORY IS A RELEASED HANDLE (clause 6.7), and closing it
+ // released the iterator too, which fs.h states. Both are refused here
+ // without the iterator being read through.
+ const int fd = okl::unpack(d.h);
+ if (fd < 0 || iter == nullptr || *iter == 0) return kal_err_invalid;
auto* s = reinterpret_cast(*iter);
+ if (!listing_take(s, fd)) return kal_err_invalid;
for (;;) {
if (s->pos >= s->used) {
const okl_long r = okl::sys(okl::nr_getdents64, s->fd,
@@ -532,8 +630,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter,
static_cast(sizeof s->buf));
if (okl::interrupted(r)) continue;
if (okl::failed(r) || r == 0) {
- okl::sys(okl::nr_close, s->fd);
- kal_free(s, sizeof(listing), alignof(listing));
+ listing_give(s, true);
*iter = 0;
if (name_len) *name_len = 0;
return okl::failed(r) ? okl::translate(r) : kal_ok;
@@ -551,6 +648,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter,
if (kind) *kind = e->type == okl::dt_dir ? kal_node_directory
: e->type == okl::dt_reg ? kal_node_file
: e->type == okl::dt_lnk ? kal_node_link : kal_node_other;
+ listing_give(s, false);
return kal_ok;
}
}
diff --git a/src/sys.h b/src/sys.h
index 9ffe77c..53006e0 100644
--- a/src/sys.h
+++ b/src/sys.h
@@ -534,6 +534,14 @@ enum : okl_u32 {
t_isig = 0000001u, t_icanon = 0000002u, t_echo = 0000010u, t_iexten = 0100000u,
};
enum : okl_u32 { t_ixon = 0002000u };
+// `t_istrip', `t_inlcr', `t_igncr' and `t_icrnl' are the mechanisms by which
+// this kernel alters input on its way: clearing the high bit, rewriting a line
+// feed into a carriage return, discarding a carriage return, and rewriting a
+// carriage return into a line feed. KAL_TERM_PASS_INPUT is the whole of them,
+// for the reason given above for the three that keep a keystroke. The last is
+// the one this kernel applies ordinarily, and the one that made the return key
+// and Ctrl+J the same byte (mcpplibs/openkal#48).
+enum : okl_u32 { t_istrip = 0000040u, t_inlcr = 0000100u, t_igncr = 0000200u, t_icrnl = 0000400u };
enum { v_time = 5, v_min = 6 };
// --- openkal.net and openkal.datagram --------------------------------------
diff --git a/src/task.cpp b/src/task.cpp
index 35017e5..2c24e7c 100644
--- a/src/task.cpp
+++ b/src/task.cpp
@@ -107,13 +107,49 @@ extern "C" okl_long __okl_clone(int (*fn)(void*), void* stack, int flags, void*
namespace {
-constexpr okl_uptr kStack = 256u * 1024u;
+// THE SIZE A CONTEXT IS GIVEN WHEN IT ASKS FOR NONE, AND IT IS THE HOST'S.
+//
+// It was 256 KiB, chosen when nothing above asked, and a C library above then
+// ran every thread on it whatever `pthread_attr_setstacksize' had been told
+// (mcpplibs/openkal-musl#48). A context that asks for nothing is the common
+// case --- C++'s `std::thread' has no way to ask --- so the default is the one a
+// program finds on the host's own C library, which sizes a thread by the limit
+// it gives the program's first stack: 8 MiB. The mapping is not committed until
+// it is touched, so a larger default costs address space and not memory.
+constexpr okl_uptr kDefaultStack = 8u * 1024u * 1024u;
+
+// THE GUARD BELOW EVERY STACK THIS IMPLEMENTATION MAPS, AND WHY IT IS LARGE.
+//
+// Without one, a context that ran off the end of its stack wrote into whatever
+// mapping lay beneath --- another context's stack, a block of the allocator ---
+// and nothing stopped it: the region was one anonymous mapping from kal_alloc,
+// and the C library above cannot place a guard of its own, because openkal has
+// no operation upon the protection of memory. A guard is only as good as the
+// largest frame that can step over it without touching it, and a frame of
+// 69,008 bytes was measured in the C library above in an unoptimised build, so
+// one page would be stepped over by the first such call. 256 KiB is address
+// space and not memory.
+constexpr okl_uptr kGuard = 256u * 1024u;
+
+constexpr okl_uptr kPageBytes = 4096u;
+
+okl_uptr round_up(okl_uptr n, okl_uptr to) { return (n + to - 1) & ~(to - 1); }
+
+// What a request for `want' bytes is given: at least that, and never less than
+// the default, which is what kal_task_start_sized states ("a least size").
+okl_uptr stack_for(okl_uptr want) {
+ if (want > ~okl_uptr{0} - kPageBytes - kGuard) return 0;
+ const okl_uptr w = round_up(want, kPageBytes);
+ return w > kDefaultStack ? w : kDefaultStack;
+}
struct context {
void (*entry)(void*);
void* arg;
- void* stack;
- okl_uptr stack_bytes;
+ void* stack; // the lowest usable address, above the guard
+ okl_uptr stack_bytes; // the usable size, which kal_task_stack reports
+ void* map; // the mapping, guard included
+ okl_uptr map_bytes;
okl::tls_block tls;
volatile int tid; // the kernel clears this when the context ends
#ifndef OKL_STANDALONE
@@ -407,23 +443,47 @@ int translate_posix(int e) {
#endif
-} // namespace
+#ifdef OKL_STANDALONE
+// Maps a stack of `bytes' usable bytes with the guard beneath it. The guard is
+// made inaccessible rather than left unmapped, so that nothing the kernel or
+// another allocation places later can occupy the addresses beneath the stack.
+bool map_stack(context* c, okl_uptr bytes) {
+ const okl_uptr total = bytes + kGuard;
+ const okl_long r = okl::sys(okl::nr_mmap, 0, static_cast(total),
+ okl::prot_read | okl::prot_write,
+ okl::map_private | okl::map_anonymous | okl::map_stack, -1, 0);
+ if (okl::failed(r)) return false;
+ if (okl::failed(okl::sys(okl::nr_mprotect, r, static_cast(kGuard), okl::prot_none))) {
+ okl::sys(okl::nr_munmap, r, static_cast(total));
+ return false;
+ }
+ c->map = reinterpret_cast(r);
+ c->map_bytes = total;
+ c->stack = reinterpret_cast(r) + kGuard;
+ c->stack_bytes = bytes;
+ return true;
+}
-extern "C" {
+void unmap_stack(context* c) {
+ if (c->map) okl::sys(okl::nr_munmap, reinterpret_cast(c->map),
+ static_cast(c->map_bytes));
+}
+#endif
-int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) {
+int start(void (*entry)(void*), void* arg, okl_uptr want, kal_task* out) {
if (entry == nullptr || out == nullptr) return kal_err_invalid;
+ const okl_uptr bytes = stack_for(want);
+ if (bytes == 0) return kal_err_no_memory;
auto* c = static_cast(kal_alloc(sizeof(context), alignof(context)));
if (c == nullptr) return kal_err_no_memory;
okl::fill(c, 0, sizeof(context));
c->entry = entry; c->arg = arg;
#ifdef OKL_STANDALONE
- c->stack_bytes = kStack;
- c->stack = kal_alloc(kStack, 16);
- c->tls = okl::make_tls(alloc_bridge);
- if (c->stack == nullptr || c->tls.tp == nullptr) {
- if (c->stack) kal_free(c->stack, kStack, 16);
+ const bool mapped = map_stack(c, bytes);
+ c->tls = okl::make_tls(alloc_bridge);
+ if (!mapped || c->tls.tp == nullptr) {
+ unmap_stack(c);
if (c->tls.base) kal_free(c->tls.base, c->tls.bytes, c->tls.align);
kal_free(c, sizeof(context), alignof(context));
return kal_err_no_memory;
@@ -441,20 +501,32 @@ int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) {
| 0x00080000 // CLONE_SETTLS
| 0x00100000 // CLONE_PARENT_SETTID
| 0x00200000; // CLONE_CHILD_CLEARTID
- auto* top = static_cast(c->stack) + kStack;
+ auto* top = static_cast(c->stack) + c->stack_bytes;
const okl_long r = __okl_clone(run, top, flags, c,
const_cast(&c->tid), c->tls.tp,
const_cast(&c->tid));
if (okl::failed(r)) {
- kal_free(c->stack, kStack, 16);
+ unmap_stack(c);
kal_free(c->tls.base, c->tls.bytes, c->tls.align);
kal_free(c, sizeof(context), alignof(context));
return okl::translate(r);
}
#else
+ // THE RUNTIME OWNS THE STACK HERE, so the size is asked of it. Its own
+ // accounting takes the guard and its thread-local image out of the size it
+ // is given, which is why the request is padded: what the context is told by
+ // kal_task_stack must be at least what was asked for, and the runtime's
+ // deductions are its own.
+ pthread_attr_t attr;
+ if (::pthread_attr_init(&attr) != 0) { kal_free(c, sizeof(context), alignof(context)); return kal_err_no_memory; }
+ const int sized = want == 0 ? 0 : ::pthread_attr_setstacksize(&attr, bytes + kGuard);
pthread_t id{};
- const int rc = ::pthread_create(&id, nullptr, run, c);
- if (rc != 0) { kal_free(c, sizeof(context), alignof(context)); return translate_posix(rc); }
+ const int rc = sized != 0 ? sized : ::pthread_create(&id, want == 0 ? nullptr : &attr, run, c);
+ ::pthread_attr_destroy(&attr);
+ if (rc != 0) {
+ kal_free(c, sizeof(context), alignof(context));
+ return rc == okl::e_inval && want != 0 ? kal_err_no_memory : translate_posix(rc);
+ }
c->thread = static_cast(id);
#endif
@@ -462,6 +534,22 @@ int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) {
return kal_ok;
}
+} // namespace
+
+extern "C" {
+
+int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) {
+ return start(entry, arg, 0, out);
+}
+
+// A least size, version 0.16. The size given is at least the one asked for and
+// never less than the default, so a C library that asks for its own default on
+// every thread does not shrink what each one had.
+int kal_task_start_sized(void (*entry)(void*), void* arg, kal_uintptr stack_size,
+ kal_task* out) {
+ return start(entry, arg, stack_size, out);
+}
+
int kal_task_join(kal_task h) {
auto* c = reinterpret_cast(h.h);
if (c == nullptr) return kal_err_invalid;
@@ -475,7 +563,7 @@ int kal_task_join(kal_task h) {
okl::sys(okl::nr_futex, reinterpret_cast(&c->tid),
okl::futex_wait, t, 0, 0, 0);
}
- kal_free(c->stack, c->stack_bytes, 16);
+ unmap_stack(c);
kal_free(c->tls.base, c->tls.bytes, c->tls.align);
#else
const int rc = ::pthread_join(static_cast(c->thread), nullptr);
diff --git a/src/terminal.cpp b/src/terminal.cpp
index dcedc7e..ddba069 100644
--- a/src/terminal.cpp
+++ b/src/terminal.cpp
@@ -25,12 +25,18 @@ namespace {
// which some of them had been released reads as clear and is restored to the
// set this kernel ordinarily reserves, which is the cost the specification
// records beside the position.
+// KAL_TERM_PASS_INPUT IS READ FROM FOUR FLAGS, for the same reason: it states
+// that nothing typed is altered, which holds only where every mechanism by which
+// this kernel alters input is off.
+constexpr okl_u32 kAlters = okl::t_istrip | okl::t_inlcr | okl::t_igncr | okl::t_icrnl;
+
kal_uintptr mode_of(const okl::ktermios& t) {
kal_uintptr m = 0;
if ((t.lflag & okl::t_icanon) != 0) m |= KAL_TERM_LINE_EDIT;
if ((t.lflag & okl::t_echo) != 0) m |= KAL_TERM_ECHO;
if ((t.lflag & (okl::t_isig | okl::t_iexten)) == 0 &&
(t.iflag & okl::t_ixon) == 0) m |= KAL_TERM_PASS_CONTROL;
+ if ((t.iflag & kAlters) == 0) m |= KAL_TERM_PASS_INPUT;
return m;
}
@@ -93,6 +99,14 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) {
}
}
+ // The same rule, for the position that stands for four flags. Clearing it
+ // restores what this kernel ordinarily applies, which is the rewriting of a
+ // carriage return into a line feed and none of the other three.
+ if (((mode ^ in_effect) & KAL_TERM_PASS_INPUT) != 0) {
+ if ((mode & KAL_TERM_PASS_INPUT) != 0) t.iflag &= ~kAlters;
+ else t.iflag |= okl::t_icrnl;
+ }
+
// AND A MODE IS NOT A WAY TO END THE INPUT. With line assembly off, how
// long a read waits is decided by VMIN and VTIME rather than by a newline,
// and a terminal left at VMIN=0 by whatever ran before makes
diff --git a/tests/conformance_fs.cpp b/tests/conformance_fs.cpp
index 0213f32..a09a7c8 100644
--- a/tests/conformance_fs.cpp
+++ b/tests/conformance_fs.cpp
@@ -5,6 +5,7 @@
// makes a program confinable by the directory it was given.
import openkal.fs;
import openkal.stream;
+import openkal.task;
namespace {
int failures = 0;
@@ -95,6 +96,108 @@ int main() {
check(kal_fs_open(d, "inner", 5, kal::fs::open::read.bits, &after) != kal_ok,
"a released handle is not treated as valid");
+ // An enumeration left part-way is ended by closing the directory that
+ // produced it, which fs.h states. Counted in the kernel's own table, through
+ // the whole file system this implementation supplies as a directory.
+ {
+ kal_dir slash{}; bool have_root = false;
+ for (kal_uintptr i = 0; i < kal::fs::preopen_count(); ++i) {
+ char nm[512]; kal_uintptr nl = 0; kal_dir p{};
+ if (kal_fs_preopen(i, &p, nm, sizeof nm, &nl) == kal_ok && nl == 1 && nm[0] == '/') {
+ slash = p; have_root = true;
+ }
+ }
+ auto open_now = [&]() -> int {
+ kal_dir fds{}; kal_uintptr it = 0; int n = 0;
+ if (kal_fs_open_dir(slash, "proc/self/fd", 12, &fds) != kal_ok) return -1;
+ if (kal_fs_list_begin(fds, &it) == kal_ok) {
+ for (;;) {
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ if (kal_fs_list_next(fds, &it, name, sizeof name, &len, &kind) != kal_ok || it == 0) break;
+ ++n;
+ }
+ }
+ kal_fs_close_dir(fds);
+ return n;
+ };
+ if (have_root) {
+ const int before = open_now();
+ check(before >= 0, "the open descriptors are counted through proc/self/fd");
+ if (before >= 0) {
+ for (int i = 0; i < 64; ++i) {
+ kal_dir again{}; kal_uintptr it = 0;
+ if (kal_fs_open_dir(root, "okl_dir", 7, &again) != kal_ok) break;
+ if (kal_fs_list_begin(again, &it) == kal_ok) {
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ kal_fs_list_next(again, &it, name, sizeof name, &len, &kind);
+ }
+ kal_fs_close_dir(again);
+ }
+ check(open_now() == before, "closing a directory ends an enumeration left part-way");
+ }
+ }
+ }
+
+ // A closed directory is a released handle, and the iterator it produced was
+ // released with it (fs.h, version 0.16): the next call is refused without
+ // the iterator being read through.
+ {
+ kal_dir once{}; kal_uintptr it = 0;
+ if (kal_fs_open_dir(root, "okl_dir", 7, &once) == kal_ok
+ && kal_fs_list_begin(once, &it) == kal_ok) {
+ kal_fs_close_dir(once);
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ check(kal_fs_list_next(once, &it, name, sizeof name, &len, &kind) == kal_err_invalid,
+ "an enumeration of a closed directory is refused");
+ } else {
+ check(false, "a directory is enumerated so that its close can be observed");
+ }
+ kal_dir a{}, b{}; kal_uintptr ia = 0;
+ if (kal_fs_open_dir(root, "okl_dir", 7, &a) == kal_ok
+ && kal_fs_open_dir(root, "okl_dir", 7, &b) == kal_ok
+ && kal_fs_list_begin(a, &ia) == kal_ok) {
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ check(kal_fs_list_next(b, &ia, name, sizeof name, &len, &kind) == kal_err_invalid,
+ "an iterator is refused with a directory that did not produce it");
+ }
+ kal_fs_close_dir(a); kal_fs_close_dir(b);
+ }
+
+ // A close that races a read of the same enumeration does not free what the
+ // reader is using (clause 6.6). Probabilistic by nature; what it shows is
+ // that two hundred such races end with the reader told something and the
+ // program still running.
+ {
+ struct race { kal_dir d; volatile int done; volatile int told; };
+ auto reader = [](void* p) {
+ auto* r = static_cast(p);
+ kal_uintptr it = 0;
+ if (kal_fs_list_begin(r->d, &it) == kal_ok) {
+ for (;;) {
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ const int e = kal_fs_list_next(r->d, &it, name, sizeof name, &len, &kind);
+ if (e != kal_ok) { r->told = 1; break; }
+ if (it == 0 && kal_fs_list_begin(r->d, &it) != kal_ok) { r->told = 1; break; }
+ }
+ } else {
+ r->told = 1;
+ }
+ __atomic_store_n(&r->done, 1, __ATOMIC_RELEASE);
+ };
+ int ended = 0;
+ for (int i = 0; i < 200; ++i) {
+ race r{};
+ if (kal_fs_open_dir(root, "okl_dir", 7, &r.d) != kal_ok) break;
+ kal_task t{};
+ if (kal_task_start(reader, &r, &t) != kal_ok) { kal_fs_close_dir(r.d); break; }
+ for (int spin = 0; spin < i * 10; ++spin) kal_task_yield();
+ kal_fs_close_dir(r.d);
+ kal_task_join(t);
+ ended += r.told;
+ }
+ check(ended == 200, "an enumeration racing the close of its directory ends and is told so");
+ }
+
check(kal_fs_remove(root, "okl_dir/inner", 13) == kal_ok, "the inner file is removed");
check(kal_fs_remove(root, "okl_dir", 7) == kal_ok, "the directory is removed");
check(kal_fs_remove(root, "okl_probe.txt", 13) == kal_ok, "the file is removed");
diff --git a/tests/conformance_stack_size.cpp b/tests/conformance_stack_size.cpp
new file mode 100644
index 0000000..327c00c
--- /dev/null
+++ b/tests/conformance_stack_size.cpp
@@ -0,0 +1,59 @@
+// Conformance: the least stack size a started context is given (openkal 0.16).
+//
+// WHAT IS OBSERVED IS ROOM. The report this answers (mcpplibs/openkal-musl#48)
+// asked for a mebibyte, was given 256 KiB, and faulted at a depth the host's own
+// C library completed; a size that was reported and not provided would pass a
+// test that only read the number back.
+import openkal.types;
+import openkal.task;
+import openkal.stream;
+
+namespace {
+int failures = 0;
+void check(bool ok, const char* what) {
+ if (ok) return;
+ ++failures;
+ const char pre[] = "FAIL: ";
+ kal::write(kal::err(), pre, sizeof(pre) - 1);
+ kal_uintptr n = 0; while (what[n]) ++n;
+ kal::write(kal::err(), what, n);
+ kal::write(kal::err(), "\n", 1);
+}
+
+long recurse(long n);
+long (*volatile g_recurse)(long) = recurse;
+long recurse(long n) {
+ volatile char pad[1000];
+ pad[0] = static_cast(n);
+ if (n == 0) return 0;
+ return g_recurse(n - 1) + pad[0];
+}
+
+struct probe { kal_uintptr ask; kal_uintptr told; volatile int done; };
+
+void room(void* p) {
+ auto* q = static_cast(p);
+ void* base = nullptr; kal_uintptr size = 0;
+ if (kal_task_stack(&base, &size) == kal_ok) q->told = size;
+ g_recurse(static_cast(q->ask / 1024 * 3 / 4));
+ __atomic_store_n(&q->done, 1, __ATOMIC_RELEASE);
+}
+}
+
+int main() {
+ const kal_uintptr asks[] = { 1u << 20, 16u << 20, 64u << 20 };
+ for (kal_uintptr ask : asks) {
+ probe q{ ask, 0, 0 };
+ kal_task t{};
+ check(kal_task_start_sized(room, &q, ask, &t) == kal_ok, "a context with a least stack size starts");
+ kal_task_join(t);
+ check(q.told >= ask, "the context is told at least the size asked for");
+ check(q.done == 1, "the context has the room it asked for");
+ }
+ // An unmeetable request is exhaustion, and nothing is started.
+ kal_task never{};
+ check(kal_task_start_sized(room, nullptr, ~kal_uintptr{0} - 4095, &never) == kal_err_no_memory,
+ "a stack larger than the address space is reported as exhaustion");
+ if (failures == 0) kal::write(kal::out(), "openkal-linux: a least stack size is room\n", 42);
+ return failures == 0 ? 0 : 1;
+}
diff --git a/tests/conformance_terminal.cpp b/tests/conformance_terminal.cpp
new file mode 100644
index 0000000..e3b65f2
--- /dev/null
+++ b/tests/conformance_terminal.cpp
@@ -0,0 +1,106 @@
+// Conformance: openkal.terminal upon a pseudo terminal this test makes.
+//
+// WHAT IS OBSERVED IS THE BYTE, because that is what mcpplibs/openkal#48 was
+// about: the return key reached a program in raw mode as a line feed, the same
+// byte Ctrl+J produces, and nothing above this interface could tell the two
+// apart again. The conformance suite cannot make a terminal --- openkal has no
+// operation for one --- so the position is observed here, through the kernel's
+// own table of terminals, which is what clause 7.13 does for handles.
+#include
+#include
+#include
+#include
+
+import openkal.types;
+import openkal.stream;
+import openkal.terminal;
+
+namespace {
+int failures = 0;
+void check(bool ok, const char* what) {
+ if (ok) return;
+ ++failures;
+ const char pre[] = "FAIL: ";
+ kal::write(kal::err(), pre, sizeof(pre) - 1);
+ kal_uintptr n = 0; while (what[n]) ++n;
+ kal::write(kal::err(), what, n);
+ kal::write(kal::err(), "\n", 1);
+}
+
+constexpr kal_uintptr kRaw = kal::terminal::pass_control.bits; // line assembly and echo off
+constexpr tcflag_t kAlters = ISTRIP | INLCR | IGNCR | ICRNL;
+
+// What the terminal delivers for the bytes the other side writes.
+int deliver(int master, kal_stream s, const char* in, int n, unsigned char* out) {
+ if (write(master, in, n) != n) return -1;
+ int got = 0;
+ while (got < n) {
+ const kal_intptr r = kal_stream_read(s, out + got, n - got);
+ if (r <= 0) break;
+ got += static_cast(r);
+ }
+ return got;
+}
+}
+
+int main() {
+ const int master = posix_openpt(O_RDWR | O_NOCTTY);
+ if (master < 0 || grantpt(master) != 0 || unlockpt(master) != 0) {
+ kal::write(kal::err(), "no pseudo terminal; not observed\n", 33);
+ return 0;
+ }
+ const int slave = open(ptsname(master), O_RDWR | O_NOCTTY);
+ check(slave >= 0, "the pseudo terminal opens");
+ const kal_stream s{ static_cast(slave) };
+
+ // As the kernel makes one: the return key is rewritten, so the position
+ // reads clear.
+ kal_uintptr m = ~kal_uintptr{0};
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & kal::terminal::pass_input.bits) == 0,
+ "a terminal that rewrites the return key reads as altering input");
+
+ // Raw, and nothing altered: the return key is a carriage return and Ctrl+J
+ // is a line feed. This is the pair the report could not tell apart.
+ check(kal_terminal_set_mode(s, kRaw | kal::terminal::pass_input.bits) == kal_ok, "raw input is requested");
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & kal::terminal::pass_input.bits) != 0,
+ "the position reads as set once established");
+ termios t{};
+ tcgetattr(slave, &t);
+ check((t.c_iflag & kAlters) == 0, "every mechanism that alters input is off");
+ unsigned char b[4] = {};
+ check(deliver(master, s, "\r\n", 2, b) == 2 && b[0] == 0x0d && b[1] == 0x0a,
+ "the return key and Ctrl+J arrive as two different bytes");
+
+ // Cleared again: what this kernel ordinarily does, and only that.
+ check(kal_terminal_set_mode(s, kRaw) == kal_ok, "altered input is requested");
+ tcgetattr(slave, &t);
+ check((t.c_iflag & ICRNL) != 0 && (t.c_iflag & (ISTRIP | INLCR | IGNCR)) == 0,
+ "clearing the position restores the rewriting of the return key alone");
+ check(deliver(master, s, "\r", 1, b) == 1 && b[0] == 0x0a,
+ "with the position clear the return key is rewritten as before");
+
+ // A position already at the requested value is not written. ISTRIP set by
+ // someone else keeps the position clear, and asking for it clear again
+ // leaves ISTRIP and the rest exactly as they were.
+ tcgetattr(slave, &t);
+ t.c_iflag = (t.c_iflag & ~ICRNL) | ISTRIP;
+ tcsetattr(slave, TCSANOW, &t);
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & kal::terminal::pass_input.bits) == 0,
+ "one mechanism in effect is enough to read as altering input");
+ check(kal_terminal_set_mode(s, kRaw) == kal_ok, "the same mode is requested again");
+ tcgetattr(slave, &t);
+ check((t.c_iflag & ISTRIP) != 0 && (t.c_iflag & ICRNL) == 0,
+ "a position at its requested value is not written");
+
+ // Independent of line assembly, with the consequence terminal.h states: a
+ // carriage return no longer ends a line, the line feed does.
+ check(kal_terminal_set_mode(s, kal::terminal::line_edit.bits | kal::terminal::pass_input.bits) == kal_ok,
+ "line assembly with nothing altered is requested");
+ unsigned char line[8] = {};
+ check(deliver(master, s, "ab\r\n", 4, line) == 4 && line[2] == 0x0d && line[3] == 0x0a,
+ "with line assembly on, the carriage return is part of the line");
+
+ close(slave); close(master);
+ if (failures == 0) kal::write(kal::out(), "openkal-linux: the terminal's alteration of input\n", 51);
+ return failures == 0 ? 0 : 1;
+}