From 4f25f91fee37b7d7f9bac06eda4e55827038df8a Mon Sep 17 00:00:00 2001 From: Cloud_Yun Date: Sat, 17 Oct 2026 15:00:58 +0900 Subject: [PATCH 1/5] End a directory's unfinished enumerations when the directory is closed kal_fs_list_begin opens a descriptor of its own for each enumeration, and only kal_fs_list_next released it, on reaching the end. kal_fs_close_dir closed the directory's descriptor and nothing else, so an enumeration abandoned part-way kept its descriptor and buffer until the program exited, although fs.h says that closing the directory that produced an iterator releases it. Keep the listings not yet read to the end on a list, with the directory each was begun on. kal_fs_close_dir ends that directory's listings before the descriptor is closed, and kal_fs_list_next takes a listing off the list when it reaches the end or fails. The close now frees the listing, so an iterator used after its directory is closed uses freed memory instead of going on with the directory. The test begins and abandons 64 enumerations and compares the number of open descriptors before and after. It fails without the change. --- src/fs.cpp | 50 ++++++++++++++++++++++++++++++++++++---- tests/conformance_fs.cpp | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/fs.cpp b/src/fs.cpp index 01ebe71..7632482 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -254,11 +254,49 @@ 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; 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); } + +// Ends every listing begun upon `dir', or `one' alone where `dir' is negative. +// What is ended is unlinked under the lock and released after it. +void listings_end(int dir, listing* one) { + listing* done = nullptr; + listings_lock(); + for (listing** p = &g_listings; *p != nullptr;) { + listing* s = *p; + if (dir >= 0 ? s->dir == dir : s == one) { *p = s->next; s->next = done; done = s; } + else p = &s->next; + } + listings_unlock(); + while (done != nullptr) { + listing* s = done; done = s->next; + okl::sys(okl::nr_close, s->fd); + kal_free(s, sizeof(listing), alignof(listing)); + } +} + } // namespace extern "C" { @@ -312,7 +350,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, nullptr); + okl::retire(d.h); okl::sys(okl::nr_close, fd); } void kal_fs_close_file(kal_file f) { @@ -515,7 +557,8 @@ 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->used = 0; s->pos = 0; + listings_lock(); s->next = g_listings; g_listings = s; listings_unlock(); *iter = reinterpret_cast(s); return kal_ok; } @@ -532,8 +575,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)); + listings_end(-1, s); *iter = 0; if (name_len) *name_len = 0; return okl::failed(r) ? okl::translate(r) : kal_ok; diff --git a/tests/conformance_fs.cpp b/tests/conformance_fs.cpp index 0213f32..db9e587 100644 --- a/tests/conformance_fs.cpp +++ b/tests/conformance_fs.cpp @@ -95,6 +95,48 @@ 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"); + } + } + } + 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"); From 0890b30b00607efcf81f433f3930f9f1ecfef7d2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:32:04 +0900 Subject: [PATCH 2/5] A closed directory is refused by kal_fs_list_next, and a close does not free what another context is reading The listing an iterator names is found among the open ones rather than read through, so a closed directory (a released handle, clause 6.7) or a stale iterator is refused with kal_err_invalid. A listing another context is inside kal_fs_list_next with is marked by the close and released by that context on its way out, so the close does not free a buffer a getdents64 is writing into (clause 6.6). Two contexts reading one enumeration take turns. --- src/fs.cpp | 84 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/src/fs.cpp b/src/fs.cpp index 7632482..ddc5086 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -256,6 +256,8 @@ 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]; @@ -279,24 +281,71 @@ void listings_lock() { } void listings_unlock() { __atomic_store_n(&g_listings_held, 0, __ATOMIC_RELEASE); } -// Ends every listing begun upon `dir', or `one' alone where `dir' is negative. -// What is ended is unlinked under the lock and released after it. -void listings_end(int dir, listing* one) { +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 (dir >= 0 ? s->dir == dir : s == one) { *p = s->next; s->next = done; done = s; } - else p = &s->next; + 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; - okl::sys(okl::nr_close, s->fd); - kal_free(s, sizeof(listing), alignof(listing)); + 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" { @@ -353,7 +402,7 @@ void kal_fs_close_dir(kal_dir d) { 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, nullptr); + listings_end(fd); okl::retire(d.h); okl::sys(okl::nr_close, fd); } @@ -557,17 +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->dir = fd; 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, @@ -575,7 +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) { - listings_end(-1, s); + listing_give(s, true); *iter = 0; if (name_len) *name_len = 0; return okl::failed(r) ? okl::translate(r) : kal_ok; @@ -593,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; } } From 60aa4dc0ae5d6391778f8195e544cc0ee99872a4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:32:04 +0900 Subject: [PATCH 3/5] A started context's stack has a guard beneath it, is 8 MiB unless more is asked for, and kal_task_start_sized asks The stack was one anonymous mapping from kal_alloc with nothing beneath it, and the C library above cannot place a guard of its own. It is now mapped with a 256 KiB inaccessible guard below it, larger than the 69,008-byte frame measured in that library at -O0. The default is the host's, 8 MiB, which costs address space and not memory. kal_task_start_sized (openkal 0.16) gives at least the size asked for and never less than the default; above a C runtime the request is passed to it. --- src/task.cpp | 120 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 16 deletions(-) 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); From e1b7d7ed65d937d3b46010f026fed477896f2166 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:32:04 +0900 Subject: [PATCH 4/5] KAL_TERM_PASS_INPUT is the four flags by which this kernel alters input Read as set only where ISTRIP, INLCR, IGNCR and ICRNL are all clear; set, all four are cleared; clear, ICRNL is restored, which is what this kernel ordinarily applies. A position already at the requested value is not written. --- src/sys.h | 8 ++++++++ src/terminal.cpp | 14 ++++++++++++++ 2 files changed, 22 insertions(+) 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/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 From e902685a94948ab0e82ea7da25a4682c6b386c42 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:33:52 +0900 Subject: [PATCH 5/5] 0.17.0 (draft) --- written against openkal 0.16, and observed The enumeration of a closed directory is refused and a close racing a read ends it safely; a pseudo terminal this test makes delivers the return key and Ctrl+J as two bytes once KAL_TERM_PASS_INPUT is set, and the kernel's ordinary rewriting when it is clear; a context started with a least stack size is told at least that size and has the room. --- README.md | 20 +++++- mcpp.toml | 4 +- tests/conformance_fs.cpp | 61 ++++++++++++++++++ tests/conformance_stack_size.cpp | 59 +++++++++++++++++ tests/conformance_terminal.cpp | 106 +++++++++++++++++++++++++++++++ 5 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 tests/conformance_stack_size.cpp create mode 100644 tests/conformance_terminal.cpp 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/tests/conformance_fs.cpp b/tests/conformance_fs.cpp index db9e587..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; @@ -137,6 +138,66 @@ int main() { } } + // 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; +}