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; +}