From cdaf4c82deca24e1ea511d688fff368354cbc1f8 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 01:03:37 +0800 Subject: [PATCH 1/6] 0.4.0 --- adopt openkal 0.9, and report the coarser of this system's two granularities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⭐⭐ THIS SYSTEM HAS TWO MEMORY QUANTA AND IS THE REASON THE SPECIFICATION ASKS FOR ONE NUMBER RATHER THAN A PAGE SIZE. It protects memory in pages of four kilobytes and RESERVES it in units of sixty-four, so a value taken from either alone is wrong for the other. What `kal_memory_granularity' promises is that an address and a length that are multiples of it are acceptable to every operation of the interface, and the coarser of the two is the only answer that keeps that promise. A specification that had derived the number from the page size of one family of systems would have been wrong here. `kal_fs_props' takes the directory and asks this environment what the volume can do, which it reports itself --- so nothing is guessed: case sensitivity is read rather than assumed absent, and a volume that holds nodes whose content is another name is reported as holding them. ⚠️ Those nodes are REPORTED and are not MADE, and the enquiry says so before a caller tries. Creating one here requires a privilege an ordinary program does not hold or the system's developer mode, and reading one requires a control code this implementation does not yet issue. Neither is claimed, and both operations refuse rather than being absent, because a caller can ask first --- which is what makes the refusal admissible rather than the defect clause 6.2 names. The rest follows the specification: transfers return one signed word; the parameters and names are copied into the caller's buffer; `kal_node_info' carries its own size, what was filled, and an identity taken from the volume's serial number and the file's index --- two words because this environment's index is unique WITHIN a volume and one word would make two files on two volumes read as one; asking resolves a node unless told not to; typed stream handles; `kal_version' and `kal_interfaces'. Ninety-three names are exported and none other: the two `openkal.space' names are absent, which is what the word reports as well. --- mcpp.toml | 4 +- src/datagram.cpp | 35 +++++---- src/env.cpp | 43 +++++++---- src/exec.cpp | 6 +- src/fs.cpp | 189 +++++++++++++++++++++++++++++++++++++---------- src/memory.cpp | 20 +++++ src/net.cpp | 6 +- src/process.cpp | 18 ++--- src/random.cpp | 2 +- src/stream.cpp | 23 +++--- src/task.cpp | 4 +- src/time.cpp | 4 +- src/timeout.cpp | 22 +++--- src/version.cpp | 22 ++++++ src/win.h | 37 ++++++++++ src/win32.h | 16 ++++ 16 files changed, 337 insertions(+), 114 deletions(-) create mode 100644 src/version.cpp diff --git a/mcpp.toml b/mcpp.toml index aec11c6..4181ba2 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.3.0" +version = "0.4.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-windows" [dependencies] -openkal = "0.8.0" +openkal = "0.9.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/datagram.cpp b/src/datagram.cpp index aab9b1d..c150f04 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -77,22 +77,22 @@ int kal_datagram_local(kal_datagram d, kal_endpoint* out) { return okw::from_system(ss, *out); } -kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, - const kal_endpoint* to) { +kal_intptr kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, + const kal_endpoint* to) { const SOCKET s = socket_of(d); - if (bad(s) || to == nullptr) return { 0, kal_err_invalid }; - if (len > kMaxOne) return { 0, kal_err_invalid }; + if (bad(s) || to == nullptr) return -kal_err_invalid; + if (len > kMaxOne) return -kal_err_invalid; auto* n = net(); - if (n == nullptr) return { 0, kal_err_io }; + if (n == nullptr) return -kal_err_io; ksockaddr_storage ss{}; int addrlen = 0; if (const int rc = okw::to_system(*to, ss, addrlen); rc != kal_ok) - return { 0, rc }; + return -rc; const int r = n->send_to(s, static_cast(buf), static_cast(len), 0, &ss, addrlen); - if (r < 0) return { 0, okw::last_socket_error() }; + if (r < 0) return -okw::last_socket_error(); // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface // states. The system reports a count anyway; a count short of the length @@ -101,17 +101,17 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr // partial send this interface says cannot occur, so it is reported as a // failure of the medium instead. const kal_uintptr sent = static_cast(r); - return { sent, sent == len ? kal_ok : kal_err_io }; + return sent == len ? static_cast(sent) : -kal_err_io; } -kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, - kal_endpoint* from) { +kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from) { const SOCKET s = socket_of(d); - if (bad(s)) return { 0, kal_err_invalid }; + if (bad(s)) return -kal_err_invalid; if (len > kMaxOne) len = kMaxOne; auto* n = net(); - if (n == nullptr) return { 0, kal_err_io }; + if (n == nullptr) return -kal_err_io; ksockaddr_storage ss{}; int addrlen = static_cast(sizeof ss); @@ -137,9 +137,12 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, from->addr_len = 0; from->port = 0; } - return { len, kal_ok }; + // A message longer than the buffer: the bytes placed are the + // buffer's length, and reporting the count is reporting what the + // caller may read. + return static_cast(len); } - return { 0, okw::last_socket_error() }; + return -okw::last_socket_error(); } if (from != nullptr) { @@ -152,7 +155,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, from->port = 0; } } - return { static_cast(r), kal_ok }; + return static_cast(r); } void kal_datagram_close(kal_datagram d) { @@ -166,6 +169,6 @@ void kal_datagram_close(kal_datagram d) { // been set, and this interface has no operation that would set it; a word // claiming a facility no operation reaches is the disagreement clause 6.2 exists // to prevent. -const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6; +kal_uintptr kal_datagram_props(void) { return KAL_DGRAM_PROP_IPV6; } } // extern "C" diff --git a/src/env.cpp b/src/env.cpp index 9b17f25..05e7acd 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -101,15 +101,30 @@ extern "C" { kal_uintptr kal_env_arg_count(void) { prepare(); return static_cast(g_argc); } -const char* kal_env_arg(kal_uintptr index, kal_uintptr* len) { +// EVERY VALUE IS COPIED INTO THE CALLER'S BUFFER, and each reports the length +// the value HAS. These answered with a pointer into this implementation's own +// storage, which is meaningful only while the implementation shares the +// caller's address space. +namespace { +kal_intptr give(const char* v, kal_uintptr n, char* out, kal_uintptr cap) { + if (out != nullptr && cap != 0) { + const kal_uintptr room = n < cap ? n : cap; + for (kal_uintptr i = 0; i < room; ++i) out[i] = v[i]; + } + return static_cast(n); +} +} // namespace + +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) { prepare(); - if (index >= static_cast(g_argc)) { if (len) *len = 0; return nullptr; } - if (len) *len = g_argv_len[index]; - return g_argv[index]; + if (index >= static_cast(g_argc)) return -kal_err_not_found; + return give(g_argv[index], g_argv_len[index], out, cap); } -const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len) { +kal_intptr kal_env_var(const char* name, kal_uintptr name_len, + char* out, kal_uintptr cap) { prepare(); + if (name == nullptr) return -kal_err_invalid; for (int i = 0; i < g_varc; ++i) { // Names are compared without regard to case, because that is how this // environment compares them. A program that set PATH and asked for Path @@ -124,23 +139,19 @@ const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* val if (a != b) { equal = false; break; } } if (!equal) continue; - if (value_len) *value_len = g_value_len[i]; - return g_value[i]; + return give(g_value[i], g_value_len[i], out, cap); } - if (value_len) *value_len = 0; - return nullptr; + // A name that is not there is distinct from one whose value is empty. + return -kal_err_not_found; } kal_uintptr kal_env_var_count(void) { prepare(); return static_cast(g_varc); } -const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len, - const char** value, kal_uintptr* value_len) { +// The NAME at a position. The value is then obtained by kal_env_var. +kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) { prepare(); - if (index >= static_cast(g_varc)) return nullptr; - if (name_len) *name_len = g_name_len[index]; - if (value) *value = g_value[index]; - if (value_len) *value_len = g_value_len[index]; - return g_entry[index]; + if (index >= static_cast(g_varc)) return -kal_err_not_found; + return give(g_entry[index], g_name_len[index], out, cap); } } diff --git a/src/exec.cpp b/src/exec.cpp index 2ce0472..3c17737 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -66,6 +66,10 @@ void kal_exec_free(void* p, kal_uintptr size) { // protection call is not one-way, and a second `VirtualProtect' to // PAGE_READWRITE succeeds. The position is set accordingly, and a caller that // must change published bytes need not abandon the region. -const kal_uintptr kal_exec_props = KAL_EXEC_PROP_REPUBLISH; +// This system grants executable memory to every program; nothing here is +// withheld from an artifact for the way it was produced. +kal_uintptr kal_exec_props(void) { + return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE; +} } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index 67a3624..539f946 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -121,7 +121,29 @@ long open_relative(void* root, const char* name, kal_uintptr len, nullptr, 0); } -int fill(void* h, kal_node_info* out) { +// The caller must state how much of the structure exists on its side. +bool info_ok(const kal_node_info* out) { + return out != nullptr && out->self_size >= sizeof(kal_u32) * 2; +} + +void put_bytes(void* dst, const void* src, kal_uintptr n) { + auto* d = static_cast(dst); + const auto* s = static_cast(src); + for (kal_uintptr i = 0; i < n; ++i) d[i] = s[i]; +} + +// Copies a name into a caller's buffer and reports the length it HAS. +kal_uintptr put_name(const char* src, kal_uintptr n, + char* out, kal_uintptr cap, kal_uintptr* len) { + if (out != nullptr && cap != 0) put_bytes(out, src, n < cap ? n : cap); + if (len) *len = n; + return n; +} + +// Writes no more of the structure than the caller says exists on its side, and +// reports which fields it filled. +int fill(void* h, kal_u32 wanted, kal_node_info* out) { + (void)wanted; okw::io_status_block s{}; okw::file_basic_information basic{}; okw::file_standard_information standard{}; @@ -132,15 +154,52 @@ int fill(void* h, kal_node_info* out) { okw::file_standard_information_class); if (!okw::ok(r)) return okw::translate_nt(r); - out->size = static_cast(standard.end_of_file); - out->modified_ns = to_nanoseconds(basic.last_write_time); - out->kind = (basic.attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link - : (basic.attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory - : kal_node_file; - out->writable = (basic.attributes & FILE_ATTRIBUTE_READONLY) ? 0 : 1; + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_KIND | KAL_INFO_SIZE | KAL_INFO_MODIFIED + | KAL_INFO_WRITABLE; + v.size = static_cast(standard.end_of_file); + v.modified_ns = to_nanoseconds(basic.last_write_time); + v.kind = (basic.attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link + : (basic.attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory + : kal_node_file; + v.writable = (basic.attributes & FILE_ATTRIBUTE_READONLY) ? 0 : 1; + + // ⭐ THE IDENTITY IS TWO WORDS BECAUSE ONE IS NOT ENOUGH, AND THIS + // ENVIRONMENT SAYS SO ITSELF: the index it keeps for a file is unique + // WITHIN A VOLUME, so two files on two volumes can share one. The volume's + // serial number is the other word. Where either enquiry is refused --- a + // handle to something that is not on a volume --- the position is left + // clear and a caller is told that this is not known, rather than being told + // that two different nodes are the same. + okw::file_internal_information index{}; + okw::file_fs_volume_information volume{}; + const long ri = okw::NtQueryInformationFile(h, &s, &index, sizeof index, + okw::file_internal_information_class); + const long rv = okw::NtQueryVolumeInformationFile(h, &s, &volume, sizeof volume, + okw::fs_volume_information_class); + if (okw::ok(ri) && okw::ok(rv)) { + v.identity[0] = static_cast(volume.serial_number); + v.identity[1] = static_cast(index.index_number); + v.present |= KAL_INFO_IDENTITY; + } + + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + put_bytes(out, &v, n); return kal_ok; } +void fill_absent(kal_node_info* out) { + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_KIND; + v.kind = kal_node_absent; + const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; + put_bytes(out, &v, n); +} + // Enumeration holds a buffer and a handle of its own, obtained by opening the // directory through itself: an enumeration that shared the caller's handle // would share its position, and two enumerations of one directory would consume @@ -171,14 +230,14 @@ extern "C" { kal_uintptr kal_fs_preopen_count(void) { kal_uintptr n = 0; table(&n); return n; } -int kal_fs_preopen(kal_uintptr index, kal_dir* out, const char** name, kal_uintptr* len) { +int kal_fs_preopen(kal_uintptr index, kal_dir* out, + char* name_out, kal_uintptr name_cap, kal_uintptr* name_len) { kal_uintptr n = 0; preopen* t = table(&n); if (index >= n || out == nullptr) return kal_err_invalid; if (t[index].handle == 0) return kal_err_permission; *out = kal_dir{ t[index].handle }; - if (name) *name = t[index].name; - if (len) *len = t[index].len; + put_name(t[index].name, t[index].len, name_out, name_cap, name_len); return kal_ok; } @@ -233,14 +292,6 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, return kal_ok; } -int kal_fs_open_file(kal_dir base, const char* name, kal_uintptr len, - int write, int create, kal_file* out) { - kal_uintptr flags = KAL_OPEN_READ; - if (write) flags |= KAL_OPEN_WRITE; - if (create) flags |= KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE; - return kal_fs_open(base, name, len, flags, out); -} - void kal_fs_close_dir(kal_dir d) { void* h = dir_handle(d); if (h) { okw::retire(d.h); okw::NtClose(h); } @@ -254,11 +305,14 @@ void kal_fs_close_file(kal_file f) { // A file's stream is the file. The environment's handle is what openkal.stream // holds here, so no conversion is required and none is performed --- which is a // property of this implementation rather than of the specification. -kal_uintptr kal_fs_stream(kal_file f) { +kal_stream kal_fs_stream(kal_file f) { void* h = file_handle(f); - return h ? reinterpret_cast(h) : 0u; + return kal_stream{ h ? reinterpret_cast(h) : 0u }; } +// The greatest length of a name this implementation accepts. +kal_uintptr kal_fs_max_name(void) { return okw::kMaxName - 1; } + int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) { void* h = file_handle(f); if (!h) return kal_err_invalid; @@ -286,32 +340,40 @@ int kal_fs_truncate(kal_file f, kal_u64 size) { return okw::ok(r) ? kal_ok : okw::translate_nt(r); } -int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_node_info* out) { +int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, + kal_uintptr flags, kal_u32 wanted, kal_node_info* out) { void* root = dir_handle(base); - if (!root || out == nullptr || !okw::acceptable(name, len)) return kal_err_invalid; + if (!root || !info_ok(out) || !okw::acceptable(name, len)) return kal_err_invalid; void* h = nullptr; + // RESOLVES BY DEFAULT, SO THAT ASKING AND OPENING ANSWER THE SAME QUESTION. + // Without FILE_OPEN_REPARSE_POINT this environment follows the node to what + // it finally refers to, which is what `kal_fs_open' does; with it, the node + // itself is opened and reported. + const unsigned long options = okw::file_open_for_backup_intent + | ((flags & KAL_FS_NO_RESOLVE) ? okw::file_open_reparse_point : 0u); const long r = open_relative(root, name, len, FILE_READ_ATTRIBUTES, - okw::file_open, okw::file_open_for_backup_intent, &h); + okw::file_open, options, &h); if (!okw::ok(r)) { // Clause 7.7: a name that does not exist is an answer, not a failure. A // caller that asks what a name refers to has been answered when told - // that it refers to nothing. + // that it refers to nothing --- and so is a node whose content names + // something absent, when the enquiry resolves. const int e = okw::translate_nt(r); if (e == kal_err_not_found || e == kal_err_not_directory) { - *out = kal_node_info{ 0, 0, kal_node_absent, 0 }; + fill_absent(out); return kal_ok; } return e; } - const int e = fill(h, out); + const int e = fill(h, wanted, out); okw::NtClose(h); return e; } -int kal_fs_file_info(kal_file f, kal_node_info* out) { +int kal_fs_file_info(kal_file f, kal_u32 wanted, kal_node_info* out) { void* h = file_handle(f); - if (!h || out == nullptr) return kal_err_invalid; - return fill(h, out); + if (!h || !info_ok(out)) return kal_err_invalid; + return fill(h, wanted, out); } int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { @@ -421,8 +483,9 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { return kal_ok; } -int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, - kal_uintptr* len, int* kind) { +int kal_fs_list_next(kal_dir, 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; auto* s = reinterpret_cast(*iter); @@ -438,8 +501,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, okw::NtClose(s->handle); kal_free(s, sizeof(listing), alignof(listing)); *iter = 0; - if (name) *name = nullptr; - if (len) *len = 0; + if (name_len) *name_len = 0; // The end of a directory is reported here as a distinct status // and is the ordinary outcome, not a failure. return (static_cast(r) == 0x80000006u) ? kal_ok @@ -462,8 +524,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, if (chars == 2 && e->file_name[0] == L'.' && e->file_name[1] == L'.') continue; const okw_uptr n = okw::narrow(e->file_name, chars, s->reported, sizeof s->reported); - if (name) *name = s->reported; - if (len) *len = n; + put_name(s->reported, n, name_out, name_cap, name_len); if (kind) *kind = (e->file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link : (e->file_attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory : kal_node_file; @@ -471,11 +532,57 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, const char** name, } } -// Names on this system are compared without regard to case, and a program that -// created two names differing only in case would succeed on one implementation -// and not on this one. The position reports it in advance, which no operation -// could. -const kal_uintptr kal_fs_props = - KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; +// The properties of the volume a directory is on. +// +// AN ENQUIRY TAKING THE RESOURCE, BECAUSE EVERY POSITION IS A PROPERTY OF THE +// FORMAT. This environment reports the volume's abilities itself, so nothing is +// guessed: names on the volume this system is ordinarily installed on are +// compared without regard to case, and a volume attached to the same machine +// may be otherwise --- and a word per implementation could state neither. +kal_uintptr kal_fs_props(kal_dir d) { + void* h = dir_handle(d); + const kal_uintptr conservative = + KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; + if (!h) return 0; + + okw::io_status_block s{}; + struct { okw::file_fs_attribute_information info; wchar_t rest[64]; } a{}; + const long r = okw::NtQueryVolumeInformationFile(h, &s, &a, sizeof a, + okw::fs_attribute_information_class); + if (!okw::ok(r)) return conservative; + + kal_uintptr p = conservative; + if (a.info.attributes & okw::fs_case_sensitive_search) p |= KAL_FS_PROP_CASE_SENSITIVE; + + // ⚠️ LINKS ARE REPORTED AND ARE NOT MADE, AND THE ASYMMETRY IS THIS + // IMPLEMENTATION'S RATHER THAN THE SPECIFICATION'S. + // + // A volume that supports reparse points holds nodes whose content is + // another name, and `kal_fs_info' reports one when it meets it --- so the + // position for meeting them is claimed. Creating one on this system + // requires a privilege an ordinary program does not hold, or the developer + // mode of the system; and reading one requires a control code this + // implementation does not yet issue. Neither is claimed, so a caller asks + // and is told before it tries, which is what the enquiry is for. + if (a.info.attributes & okw::fs_supports_reparse_points) p |= KAL_FS_PROP_LINKS; + return p; +} + +// Nodes whose content is another name. +// +// Refused, and the enquiry above says so in advance. Creating one on this +// system requires SeCreateSymbolicLinkPrivilege or the system's developer mode, +// and reading one requires a file-system control code this implementation does +// not issue. A caller reads KAL_FS_PROP_MAKE_LINKS --- which is not claimed +// here --- rather than discovering it by the attempt. +int kal_fs_link_create(kal_dir, const char*, kal_uintptr, + const char*, kal_uintptr, kal_uintptr) { + return kal_err_not_supported; +} + +kal_intptr kal_fs_link_read(kal_dir, const char*, kal_uintptr, + char*, kal_uintptr) { + return -kal_err_not_supported; +} } diff --git a/src/memory.cpp b/src/memory.cpp index 597bccd..b25c212 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -61,4 +61,24 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { HeapFree(h, 0, reinterpret_cast(p)[-1]); } + +// The quantum this environment allocates and protects memory in. +// +// ⭐⭐ THIS SYSTEM HAS TWO, AND THE COARSER IS REPORTED. It protects memory in +// pages of four kilobytes and RESERVES it in units of sixty-four --- so a value +// taken from either alone is wrong for the other, and a specification that +// derived one number from the page size of one family of systems would be wrong +// here. What the operation promises is that an address and a length that are +// multiples of the reported value are acceptable to every operation of the +// interface, so the coarser of the two is the only answer that keeps the +// promise. +kal_uintptr kal_memory_granularity(void) { + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const kal_uintptr page = info.dwPageSize ? info.dwPageSize : 4096u; + const kal_uintptr grain = info.dwAllocationGranularity + ? info.dwAllocationGranularity : 65536u; + return page > grain ? page : grain; +} + } diff --git a/src/net.cpp b/src/net.cpp index d4daba6..2bcaca9 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -139,12 +139,12 @@ int kal_net_accept(kal_net_listener l, kal_net_conn* out) { return kal_ok; } -kal_uintptr kal_net_stream(kal_net_conn c) { +kal_stream kal_net_stream(kal_net_conn c) { // The socket itself, for the reason kal_fs_stream gives: openkal.stream's // operations take whatever this system's transfer calls take, and a packed // word is not that. const SOCKET s = socket_of(c); - return bad(s) ? 0u : static_cast(s); + return kal_stream{ bad(s) ? 0u : static_cast(s) }; } int kal_net_peer(kal_net_conn c, kal_endpoint* out) { @@ -197,6 +197,6 @@ void kal_net_close_listener(kal_net_listener l) { // Both positions hold on this system: it speaks IPv6, and its `shutdown' ends // transfer in one direction while the other continues. -const kal_uintptr kal_net_props = KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; +kal_uintptr kal_net_props(void) { return KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; } } // extern "C" diff --git a/src/process.cpp b/src/process.cpp index 8af7600..c2404db 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -152,14 +152,14 @@ int kal_process_spawn(kal_dir base, STARTUPINFOW startup{}; startup.cb = sizeof startup; bool inherit = false; - if (streams && (streams->in || streams->out || streams->err)) { + if (streams && (streams->in.h || streams->out.h || streams->err.h)) { startup.dwFlags = STARTF_USESTDHANDLES; - startup.hStdInput = streams->in ? reinterpret_cast(streams->in) - : GetStdHandle(STD_INPUT_HANDLE); - startup.hStdOutput = streams->out ? reinterpret_cast(streams->out) - : GetStdHandle(STD_OUTPUT_HANDLE); - startup.hStdError = streams->err ? reinterpret_cast(streams->err) - : GetStdHandle(STD_ERROR_HANDLE); + startup.hStdInput = streams->in.h ? reinterpret_cast(streams->in.h) + : GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = streams->out.h ? reinterpret_cast(streams->out.h) + : GetStdHandle(STD_OUTPUT_HANDLE); + startup.hStdError = streams->err.h ? reinterpret_cast(streams->err.h) + : GetStdHandle(STD_ERROR_HANDLE); SetHandleInformation(startup.hStdInput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); SetHandleInformation(startup.hStdOutput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); SetHandleInformation(startup.hStdError, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); @@ -290,9 +290,9 @@ void kal_process_close(kal_process p) { // KAL_PROCESS_PROP_GRANT_DIR is deliberately absent: kal_process_spawn_with // refuses a non-empty set of grants here, and a word claiming a facility the // next call refuses is the disagreement clause 6.2 exists to prevent. -const kal_uintptr kal_process_props = +kal_uintptr kal_process_props(void) { return KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING | KAL_PROCESS_PROP_EXIT_STATUS - | KAL_PROCESS_PROP_CHANNEL; + | KAL_PROCESS_PROP_CHANNEL; } } diff --git a/src/random.cpp b/src/random.cpp index 3882060..7f9d547 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -83,6 +83,6 @@ int kal_random_fill(void* out, kal_uintptr len) { // Neither blocking nor hardware. The system's generator is seeded before a // process runs, so there is no wait to report; and whether the seed came from a // hardware source is not something this backend can observe. -const kal_uintptr kal_random_props = 0; +kal_uintptr kal_random_props(void) { return 0; } } // extern "C" diff --git a/src/stream.cpp b/src/stream.cpp index 05a57cb..b62b0c2 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -17,9 +17,10 @@ kal_stream kal_stdin (void) { return kal_stream{ reinterpret_cast(G kal_stream kal_stdout(void) { return kal_stream{ reinterpret_cast(GetStdHandle(STD_OUTPUT_HANDLE)) }; } kal_stream kal_stderr(void) { return kal_stream{ reinterpret_cast(GetStdHandle(STD_ERROR_HANDLE)) }; } -kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { +// ONE SIGNED WORD: the count, or the negated condition when no byte moved. +kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { void* h = handle_of(s); - if (!valid(h)) return { 0, kal_err_invalid }; + if (!valid(h)) return -kal_err_invalid; const auto* p = static_cast(buf); kal_uintptr done = 0; while (done < len) { @@ -30,17 +31,19 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { const kal_uintptr want = len - done; const DWORD chunk = want > 0x7fffffffu ? 0x7fffffffu : static_cast(want); DWORD written = 0; - if (!WriteFile(h, p + done, chunk, &written, nullptr)) - return { done, okw::translate_win32(GetLastError()) }; + if (!WriteFile(h, p + done, chunk, &written, nullptr)) { + if (done != 0) return static_cast(done); + return -okw::translate_win32(GetLastError()); + } if (written == 0) break; done += written; } - return { done, done == len ? kal_ok : kal_err_io }; + return static_cast(done); } -kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { +kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { void* h = handle_of(s); - if (!valid(h)) return { 0, kal_err_invalid }; + if (!valid(h)) return -kal_err_invalid; const DWORD want = len > 0x7fffffffu ? 0x7fffffffu : static_cast(len); DWORD got = 0; if (!ReadFile(h, buf, want, &got, nullptr)) { @@ -48,12 +51,12 @@ kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr len) { // The end of a pipe whose other side has gone is the end of input, and // this environment reports it as a failure. A caller that could not // tell the two apart would treat every completed transfer as broken. - if (e == ERROR_BROKEN_PIPE || e == ERROR_HANDLE_EOF) return { 0, kal_ok }; - return { 0, okw::translate_win32(e) }; + if (e == ERROR_BROKEN_PIPE || e == ERROR_HANDLE_EOF) return 0; + return -okw::translate_win32(e); } // A short read is reported as it occurred: unlike a short write it carries // information the caller requires, and zero denotes the end of input. - return { got, kal_ok }; + return static_cast(got); } int kal_stream_flush(kal_stream s) { diff --git a/src/task.cpp b/src/task.cpp index 1604007..3873088 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -119,8 +119,8 @@ int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { // that compiled the program: this environment's loader establishes it for every // thread it creates, which is why the position can be reported here without // this implementation doing anything to earn it. -const kal_uintptr kal_task_props = +kal_uintptr kal_task_props(void) { return KAL_TASK_PROP_PREEMPTIVE | KAL_TASK_PROP_PARALLEL - | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; + | KAL_TASK_PROP_WAIT_TIMEOUT | KAL_TASK_PROP_THREAD_LOCAL; } } diff --git a/src/time.cpp b/src/time.cpp index a22fce6..61f2095 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -73,7 +73,7 @@ void kal_time_sleep(kal_duration ns) { // The counter this environment supplies continues while the machine is // suspended, which the corresponding position records. That is the opposite of // the Linux implementation, and the difference is why the position exists. -const kal_uintptr kal_time_props = - KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_SLEEP_PRECISE; +kal_uintptr kal_time_props(void) { return + KAL_TIME_PROP_WALL_AVAILABLE | KAL_TIME_PROP_SLEEP_PRECISE; } } diff --git a/src/timeout.cpp b/src/timeout.cpp index d678801..0692694 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -186,21 +186,21 @@ int await_stream(kal_stream s, short events, kal_u64 ns) { extern "C" { -kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { +kal_intptr kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { // A transfer of zero bytes does not wait and is not bounded. Waiting first // would turn a call that always succeeds into one that can expire. - if (len == 0) return { 0, kal_ok }; + if (len == 0) return 0; if (const int rc = await_stream(s, POLLRDNORM_, ns); rc != kal_ok) - return { 0, rc }; + return -rc; return kal_stream_read(s, buf, len); } -kal_io_result kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { - if (len == 0) return { 0, kal_ok }; +kal_intptr kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { + if (len == 0) return 0; if (const int rc = await_stream(s, POLLWRNORM_, ns); rc != kal_ok) - return { 0, rc }; + return -rc; return kal_stream_write(s, buf, len); } @@ -213,12 +213,12 @@ int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) { return kal_net_accept(l, out); } -kal_io_result kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, - kal_endpoint* from, kal_u64 ns) { +kal_intptr kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from, kal_u64 ns) { const SOCKET s = okw::unpack_socket(d.h); - if (s == INVALID_SOCKET) return { 0, kal_err_invalid }; + if (s == INVALID_SOCKET) return -kal_err_invalid; - if (const int rc = await(s, POLLRDNORM_, ns); rc != kal_ok) return { 0, rc }; + if (const int rc = await(s, POLLRDNORM_, ns); rc != kal_ok) return -rc; return kal_datagram_recv_from(d, buf, len, from); } @@ -248,6 +248,6 @@ int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* termin // The bound this system distinguishes. Both `WSAPoll' and the wait upon an // object state theirs in milliseconds, and there is no call here that takes // less --- so a millisecond is what an implementation can honestly report. -const kal_uintptr kal_timeout_granularity_ns = 1000000u; +kal_u64 kal_timeout_granularity(void) { return 1000000u; } } // extern "C" diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..7e4febf --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,22 @@ +#include "win.h" +#include + +// What this implementation says about itself before it is used. Both answers are +// constants; openkal/version.h states why they belong to no interface. +extern "C" { + +kal_u64 kal_version(void) { return KAL_VERSION; } + +kal_u64 kal_interfaces(void) { + // ⚠️ `openkal.space' IS ABSENT AND THE WORD SAYS SO. This system starts a + // NAMED PROGRAM and has no primitive that copies an address space, so the + // interface is not provided at all --- a consumer that is linked learns that + // from the linker, and one bound otherwise learns it here. + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY + | KAL_IFACE_ENV | KAL_IFACE_TIME | KAL_IFACE_RANDOM + | KAL_IFACE_FS | KAL_IFACE_PROCESS | KAL_IFACE_TASK + | KAL_IFACE_EXEC | KAL_IFACE_TERMINAL | KAL_IFACE_NET + | KAL_IFACE_DATAGRAM | KAL_IFACE_TIMEOUT; +} + +} diff --git a/src/win.h b/src/win.h index 714fee7..8a6d912 100644 --- a/src/win.h +++ b/src/win.h @@ -113,8 +113,35 @@ struct file_directory_information { wchar_t file_name[1]; }; +// The index this environment keeps for a file, which is unique within a volume. +struct file_internal_information { okw_i64 index_number; }; + +// What the object manager reports about the volume a handle is on. Only the +// serial number is read; the label follows it and is not. +struct file_fs_volume_information { + okw_i64 creation_time; + unsigned long serial_number; + unsigned long label_length; + unsigned char supports_objects; + wchar_t label[1]; +}; + +// What the object manager reports about the volume's abilities. +struct file_fs_attribute_information { + unsigned long attributes; + long maximum_component_name_length; + unsigned long file_system_name_length; + wchar_t file_system_name[1]; +}; + +enum : unsigned long { + fs_case_sensitive_search = 0x00000001u, + fs_supports_reparse_points = 0x00000080u, +}; + enum : int { file_directory_information_class = 1, + file_internal_information_class = 6, file_basic_information_class = 4, file_standard_information_class = 5, file_position_information_class = 14, @@ -123,6 +150,12 @@ enum : int { file_rename_information_class = 10, }; +// The classes NtQueryVolumeInformationFile takes. +enum : int { + fs_volume_information_class = 1, + fs_attribute_information_class = 5, +}; + // The dispositions NtCreateFile takes. They are the whole of what // kal_fs_open's flags become: an intent stated once and carried out once, // rather than an open followed by a truncation that a program could stop @@ -138,6 +171,8 @@ enum : unsigned long { file_synchronous_io_nonalert = 0x00000020, file_non_directory_file = 0x00000040, file_open_for_backup_intent = 0x00004000, + // Opens the node itself rather than what its content names. + file_open_reparse_point = 0x00200000, obj_case_insensitive = 0x00000040, }; @@ -166,6 +201,8 @@ __declspec(dllimport) long __stdcall NtWriteFile(void* handle, void* event, void okw_i64* offset, unsigned long* key); __declspec(dllimport) long __stdcall NtQueryInformationFile(void* handle, io_status_block* status, void* info, unsigned long length, int cls); +__declspec(dllimport) long __stdcall NtQueryVolumeInformationFile(void* handle, io_status_block* status, + void* info, unsigned long length, int cls); __declspec(dllimport) long __stdcall NtSetInformationFile(void* handle, io_status_block* status, void* info, unsigned long length, int cls); __declspec(dllimport) long __stdcall NtQueryDirectoryFile(void* handle, void* event, void* apc, void* apc_context, diff --git a/src/win32.h b/src/win32.h index 565a96c..818bd68 100644 --- a/src/win32.h +++ b/src/win32.h @@ -332,6 +332,22 @@ OKW_IMPORT int OKW_API WideCharToMultiByte(UINT, DWORD, LPCWSTR, int, LPSTR, // specification targets, and the third is the one that matters on a processor // whose instruction path does not see the data path's writes. OKW_IMPORT LPVOID OKW_API VirtualAlloc(LPVOID, unsigned long long, DWORD, DWORD); + +// What this system reports about itself. Only two fields are read and the rest +// are named so that the record has the layout the system writes. +struct SYSTEM_INFO { + DWORD dwOemId; + DWORD dwPageSize; + LPVOID lpMinimumApplicationAddress; + LPVOID lpMaximumApplicationAddress; + unsigned long long dwActiveProcessorMask; + DWORD dwNumberOfProcessors; + DWORD dwProcessorType; + DWORD dwAllocationGranularity; + unsigned short wProcessorLevel; + unsigned short wProcessorRevision; +}; +OKW_IMPORT void OKW_API GetSystemInfo(SYSTEM_INFO*); OKW_IMPORT BOOL OKW_API VirtualProtect(LPVOID, unsigned long long, DWORD, DWORD*); OKW_IMPORT BOOL OKW_API VirtualFree(LPVOID, unsigned long long, DWORD); OKW_IMPORT BOOL OKW_API FlushInstructionCache(HANDLE, LPCVOID, unsigned long long); From 888d90d4983c623b903bcce738dd703fac08bb98 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 01:39:17 +0800 Subject: [PATCH 2/6] README: the versions it names are the versions that exist Every README here opens by showing what a program writes in its manifest, which is the first thing a reader copies and the last thing anyone edits. These lines had drifted --- the specification's own README asked for a version four minor releases old --- and nothing checked them. `openkal/tools/check-readme-versions.sh` now does. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8811ec6..b3bb09a 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. ```toml [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.3.0" +openkal-windows = "0.4.0" ``` Its purpose is as much to test the specification as to be used. openkal was From 157d31567896e088cbb408796d637ec9d97c79f9 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:13:19 +0800 Subject: [PATCH 3/6] ci: build against the specification under review, not the published one A job that resolves the published specification cannot review a change to one. Another job in this file already cloned the specification and substituted it, and that made the arrangement look complete. It was not: the steps below reached openkal by version, so a version under review -- which by definition is not published -- failed them with E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the synced index ... the index is current, so this name is either wrong or not published yet The unit is the job, not the repository. Measured across the eight repositories of this ecosystem while one change spanned all of them: seven jobs in three of them had this shape, and each of those repositories also had a job doing it correctly -- which is what made the gap invisible to a check done a repository at a time. These steps are green on main and can only be green there, because there the published version is the one under test. It is not a check that fails, it is a check that cannot run at the only time it would have something to say. Also: GetSystemInfo is declared in src/win32.h and was exported by no .def, so the completeness check named it. It belongs to kernel32. --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ .gitignore | 4 ++++ port/kernel32.def | 1 + 3 files changed, 32 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index affdb02..e4e895c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -266,6 +266,33 @@ jobs: git -C .spec checkout --quiet "origin/$branch" fi + # ⚠️⚠️ CLONING THE SPECIFICATION IS NOT THE SAME AS BUILDING AGAINST IT, + # AND THIS JOB DID THE FIRST WHILE BELIEVING IT DID THE SECOND. + # + # `.spec` above is consumed by `run-conformance.sh`, which substitutes the + # manifest itself and RESTORES IT ON EXIT --- correctly, since a script + # that rewrote a checked-in file and walked away would leave the tree + # holding a path. But the steps AFTER it call `mcpp build` directly, and + # by then the manifest names `openkal` by version again, so those steps + # resolved the PUBLISHED specification: + # + # E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the synced + # index ... the index is current, so this name is either wrong or not + # published yet + # + # ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across + # the eight repositories of this ecosystem: seven jobs in three of them + # had this shape, and each of those repositories ALSO had a job doing it + # correctly --- which is what made the gap invisible to a check done a + # repository at a time. These steps are green on `main` and can only be + # green there, because there the published version IS the one under test. + - name: Point at the specification's working tree + run: | + set -euo pipefail + sed -i 's|^openkal = .*$|openkal = { path = ".spec" }|' mcpp.toml + grep -q 'path = ".spec"' mcpp.toml \ + || { echo "::error::the specification was not substituted"; exit 1; } + - name: Install wine run: | sudo dpkg --add-architecture i386 diff --git a/.gitignore b/.gitignore index ff39a22..51060a7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ compile_commands.json # What a system leaves behind. .DS_Store Thumbs.db + +# A working tree of the specification, cloned by continuous integration. +# No trailing slash: the pattern must match a symbolic link too. +.spec diff --git a/port/kernel32.def b/port/kernel32.def index 93380a3..f109af7 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -27,6 +27,7 @@ GetLogicalDriveStringsW GetProcAddress GetProcessHeap GetStdHandle +GetSystemInfo GetSystemTimePreciseAsFileTime HeapAlloc HeapFree From c4913c5b3e4b14cff26958ffb230734dc935e186 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:16:48 +0800 Subject: [PATCH 4/6] ci: substitute the specification per step, not per job Measured 2026-08-28 across the eight repositories of this ecosystem while one change spanned all of them: eight jobs in four of them called `mcpp build' at a point where the manifest still named openkal BY VERSION, so a version under review -- which by definition is not published -- failed them with E_NOT_FOUND. The mechanism is not a missing substitution. run-conformance.sh substitutes the manifest and RESTORES IT ON EXIT, correctly; every step after it is back to naming a version. So an audit asking "does this job substitute?" passes the job and misses the steps, which is how the first pass at this found three repositories and not four. These steps are green on main and can only be green there, because there the published version is the one under test. It is not a check that fails, it is a check that cannot run at the only time it would have something to say. The substitution is also portable now: the opensbi and uefi portability jobs run on macOS and Windows, where BSD sed requires an argument to -i that GNU sed refuses. --- .github/workflows/ci.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4e895c..78f6e38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -289,9 +289,16 @@ jobs: - name: Point at the specification's working tree run: | set -euo pipefail - sed -i 's|^openkal = .*$|openkal = { path = ".spec" }|' mcpp.toml - grep -q 'path = ".spec"' mcpp.toml \ - || { echo "::error::the specification was not substituted"; exit 1; } + # ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and + # BSD sed requires an argument to -i that GNU sed refuses. A temporary + # file is the spelling that holds on all three. + subst() { # subst + sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next" + mv "$1.next" "$1" + grep -q "path = \"$2\"" "$1" \ + || { echo "::error::$1 was not substituted"; exit 1; } + } + subst mcpp.toml .spec - name: Install wine run: | From 88572c28ae607b3b4b66c645c02611ddf72ed8a4 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:25:24 +0800 Subject: [PATCH 5/6] fix: an enquiry that reports an overflow has still answered FILE_FS_VOLUME_INFORMATION ends in the volume's label, which is as long as the label is. The buffer here held the fixed part and one character of it -- enough for every field this reads, since the serial number precedes the label -- and the object manager still reported STATUS_BUFFER_OVERFLOW because the label did not fit. That value is 0x80000005: negative, so okw::ok said no, so the identity position was left clear. Which is a correct report of something that was not true. This implementation was saying "this node's identity is not known here", a caller was believing it, and the identity was sitting in the buffer. It surfaced two packages away, in openkal-musl's probe on Windows: "two different files have different identities" did not hold, because both had been given the zero this branch leaves behind. The conformance suite could not have said so and is right not to -- an implementation is allowed to decline the field, so the suite reports the observation as one it did not make. Room for a label is given so the ordinary case succeeds, and the overflow is accepted so the extraordinary one still answers. --- src/fs.cpp | 30 +++++++++++++++++++++++++++--- src/win.h | 9 +++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/fs.cpp b/src/fs.cpp index 539f946..0b0a816 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -173,14 +173,38 @@ int fill(void* h, kal_u32 wanted, kal_node_info* out) { // handle to something that is not on a volume --- the position is left // clear and a caller is told that this is not known, rather than being told // that two different nodes are the same. + // ⚠️⚠️ THE VOLUME ENQUIRY REPORTS AN OVERFLOW AND ANSWERS ANYWAY, AND + // TREATING THE OVERFLOW AS A FAILURE THREW THE ANSWER AWAY. + // + // FILE_FS_VOLUME_INFORMATION ends in the volume's LABEL, which is as long + // as the label is. A buffer holding the fixed part and one character of it + // is enough for every field this reads --- the serial number precedes the + // label --- and the object manager still reports STATUS_BUFFER_OVERFLOW, + // because the label did not fit. That value is 0x80000005: negative, so + // `okw::ok' said no, so the position was left clear. + // + // ⭐ WHICH IS A CORRECT REPORT OF SOMETHING THAT WAS NOT TRUE. The + // implementation was saying "this node's identity is not known here", a + // caller was believing it, and the identity was sitting in the buffer. It + // surfaced two packages away, in openkal-musl's probe: `two different files + // have different identities' did not hold on Windows, because both had been + // given the zero this branch leaves behind. + // + // Room for a label is given so the ordinary case SUCCEEDS, and the overflow + // is accepted so the extraordinary one still answers. Both are checked + // rather than one, because a label longer than this is a volume nobody + // tests with and the buffer would be back to reporting an overflow. + struct { + okw::file_fs_volume_information info; + wchar_t label_tail[128]; + } volume{}; okw::file_internal_information index{}; - okw::file_fs_volume_information volume{}; const long ri = okw::NtQueryInformationFile(h, &s, &index, sizeof index, okw::file_internal_information_class); const long rv = okw::NtQueryVolumeInformationFile(h, &s, &volume, sizeof volume, okw::fs_volume_information_class); - if (okw::ok(ri) && okw::ok(rv)) { - v.identity[0] = static_cast(volume.serial_number); + if (okw::ok(ri) && (okw::ok(rv) || rv == okw::status_buffer_overflow)) { + v.identity[0] = static_cast(volume.info.serial_number); v.identity[1] = static_cast(index.index_number); v.present |= KAL_INFO_IDENTITY; } diff --git a/src/win.h b/src/win.h index 8a6d912..9e05382 100644 --- a/src/win.h +++ b/src/win.h @@ -215,6 +215,15 @@ __declspec(dllimport) unsigned long __stdcall RtlNtStatusToDosError(long status) inline bool ok(long status) { return status >= 0; } +// ⚠️ AN ENQUIRY THAT REPORTS AN OVERFLOW HAS STILL ANSWERED. STATUS_BUFFER_OVERFLOW +// is a warning rather than an error: the fixed part of the structure was written +// and a variable-length tail was cut. `ok' correctly says no to it --- its sign +// bit is set --- so a caller that reads only fields preceding the tail names it +// here. Measured: `src/fs.cpp' read a volume serial number that the object +// manager had written and discarded it, and two files two packages away were +// reported to have the same identity. +inline constexpr long status_buffer_overflow = static_cast(0x80000005ul); + // --- translation ------------------------------------------------------------- // // The environment's error values are mapped onto the closed set the From c1452f4546fac80eef7152cdf26713ff2165745c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:45:24 +0800 Subject: [PATCH 6/6] fix: the .def check read one header and one spelling, and there are two of each The check's own comment records the defect it was written to prevent: three declarations added for openkal 0.8 without matching .def lines, and the failure appearing one repository away in openkal-llvm-runtime's cross-build. It happened again, with the same signature: ld.lld: error: undefined symbol: __declspec(dllimport) NtQueryVolumeInformationFile The check was right and its SCOPE was wrong, which is the harder kind to notice: it read src/win32.h alone and matched `OKW_IMPORT ... OKW_API' alone, while src/win.h declares the object manager's entries in the plain `__declspec(dllimport) long __stdcall Nt...' form. It reported a number, the number was of the names it knew about, and nothing said the set was partial. It now globs src/*.h and matches both forms: 58 declared across four headers rather than 49 across one. Four names were outside it -- NtFlushBuffersFile, NtQueryVolumeInformationFile, NtReadFile, NtWriteFile -- and all four are now exported. Only one of them was referenced, which is why only one broke a link; the other three were the same latent hazard. Verified locally by cross-building openkal-llvm-runtime's same-source example for x86_64-windows-gnu and aarch64-macos over these working trees. --- .github/workflows/ci.yml | 32 +++++++++++++++++++++++++++----- port/ntdll.def | 4 ++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78f6e38..2fd7a1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,8 +189,30 @@ jobs: # # A check that reads source files must say what they are encoded in, or # it reports on the runner's locale. - declared = set(re.findall(r'OKW_IMPORT\s+\w+\s+OKW_API\s+(\w+)\s*\(', - open("src/win32.h", encoding="utf-8").read())) + # ⚠️⚠️ EVERY HEADER, AND EVERY SPELLING. THIS CHECK MISSED THE SECOND + # OF EACH AND THE DEFECT IT WAS WRITTEN FOR HAPPENED AGAIN. + # + # It read `src/win32.h' alone and matched `OKW_IMPORT ... OKW_API' + # alone. `src/win.h' declares the object manager's entries in the + # plain form, `__declspec(dllimport) long __stdcall Nt...', so four of + # them were outside what this looked at --- and the failure appeared + # exactly where the comment above says it appeared last time, in + # openkal-llvm-runtime's cross-build, one repository away: + # + # ld.lld: error: undefined symbol: + # __declspec(dllimport) NtQueryVolumeInformationFile + # + # ⭐ THE CHECK WAS RIGHT AND ITS SCOPE WAS WRONG, which is the harder + # kind to notice: it reported a number, the number was of the names it + # knew about, and nothing said the set was partial. So the headers are + # globbed and both forms are matched. + declared = set() + headers = sorted(glob.glob("src/*.h")) + for h in headers: + text = open(h, encoding="utf-8").read() + declared |= set(re.findall(r'OKW_IMPORT\s+\w+\s+OKW_API\s+(\w+)\s*\(', text)) + declared |= set(re.findall( + r'__declspec\(dllimport\)[\w\s\*]*?(\w+)\s*\(', text)) exported = set() for f in glob.glob("port/*.def"): body = open(f, encoding="utf-8").read().split("EXPORTS", 1) @@ -203,10 +225,10 @@ jobs: print(f"::error::declared={len(declared)} exported={len(exported)}; nothing was compared") sys.exit(1) missing = sorted(declared - exported) - print(f" {len(declared)} declared, {len(exported)} exported across " - f"{len(glob.glob('port/*.def'))} .def files") + print(f" {len(declared)} declared across {len(headers)} headers, " + f"{len(exported)} exported across {len(glob.glob('port/*.def'))} .def files") if missing: - print("::error::declared in src/win32.h and exported by no .def:") + print("::error::declared in src/*.h and exported by no .def:") for m in missing: print(f" {m}") sys.exit(1) print(" ok every declared name is exported") diff --git a/port/ntdll.def b/port/ntdll.def index 4358c82..7078316 100644 --- a/port/ntdll.def +++ b/port/ntdll.def @@ -7,7 +7,11 @@ LIBRARY ntdll.dll EXPORTS NtClose NtCreateFile +NtFlushBuffersFile NtQueryDirectoryFile NtQueryInformationFile +NtQueryVolumeInformationFile +NtReadFile NtSetInformationFile +NtWriteFile RtlNtStatusToDosError