From d4feb5e8c678de98e5b45d36232269bf9a4bebc5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 18:04:04 +0900 Subject: [PATCH 1/2] tests: a node has one identity however it was reached, and a closed attenuation gives its place back; CI on build 26100 The two tests state what mcpp-language-server issue 86 found from three packages away: on Windows 11 24H2 an open file and its name had two identities, and the 65th directory attenuation of a program was refused however many had been closed. They are committed before the correction so that the run on this commit shows each failing where it should: the first on the windows-2025 row alone (build 26100, the system with the record a name is answered by), the second on every row. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 7 +- tests/attenuation_returns_its_place.cpp | 94 +++++++++++++++++++ tests/one_identity_for_a_node.cpp | 117 ++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 tests/attenuation_returns_its_place.cpp create mode 100644 tests/one_identity_for_a_node.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9b06da..95e1ed3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,13 +39,18 @@ jobs: # On the system itself. conformance: name: conformance (${{ matrix.name }}) - runs-on: windows-2022 + runs-on: ${{ matrix.os || 'windows-2022' }} timeout-minutes: 60 strategy: fail-fast: false matrix: include: - { name: 'llvm, msvc ABI', toolchain: 'llvm@20.1.7', target: '' } + # Windows 11 24H2's build (26100): the system that has the record a + # name is answered by in one request, and the only one on which a + # name's identity could differ from its open file's + # (tests/one_identity_for_a_node.cpp). + - { name: 'llvm, msvc ABI, build 26100', toolchain: 'llvm@20.1.7', target: '', os: 'windows-2025' } - { name: 'msvc', toolchain: 'msvc@system', target: '' } - { name: 'gcc, gnu CRT', toolchain: 'gcc@16.1.0', target: 'x86_64-windows-gnu' } defaults: diff --git a/tests/attenuation_returns_its_place.cpp b/tests/attenuation_returns_its_place.cpp new file mode 100644 index 0000000..a749cd9 --- /dev/null +++ b/tests/attenuation_returns_its_place.cpp @@ -0,0 +1,94 @@ +// A regression test: a directory attenuated and closed leaves nothing behind. +// +// `kal_fs_dir_attenuate` keeps a table of the trees it has made, 64 places for +// the life of the program. A place was taken by every attenuation and given +// back by none: closing the handle released what the tree had listed and kept +// the place. A program that grants the same directories, read-only, to each +// program it starts --- a language server and the engine it restarts --- was +// refused once 64 grants had been made in all, with every one of them closed: +// `kal_err_no_memory`, which its caller printed as "out of memory" on a machine +// with memory to spare (mcpp-language-server issue 86: the 22nd engine start of +// a session with three read-only grants, the 17th with four). +// +// A tree that keeps a record still holds its place until the program ends, +// which is when its record is ended; this test attenuates without one. +#include +#include +#include +#include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; + +namespace { + +void check(bool ok, const char* what) { expect(ok) << what; } + +} // namespace + +UTest attenuation_returns_its_place_test{"attenuation_returns_its_place", [] { + kal_dir cwd{}; + bool haveDir = false; + { + const kal_uintptr n = kal_fs_preopen_count(); + for (kal_uintptr i = 0; i < n && !haveDir; ++i) { + kal_dir d{}; + char nm[1024]; + kal_uintptr len = 0; + if (kal_fs_preopen(i, &d, nm, sizeof nm, &len) != kal_ok) continue; + cwd = d; + haveDir = true; + } + } + if (!haveDir) { + fail("a preopened directory to test in"); + return; + } + + // Four times the table, each attenuation closed before the next. + int refused = 0, first_refused = -1; + for (int i = 0; i < 256; ++i) { + kal_dir_rights rights{}; + rights.self_size = sizeof rights; + rights.flags = (i % 2) ? (KAL_DIR_READ_ONLY | KAL_DIR_UNCHANGING) : KAL_DIR_READ_ONLY; + kal_dir held{}; + const int rc = kal_fs_dir_attenuate(cwd, &rights, &held); + if (rc != kal_ok) { + if (first_refused < 0) { first_refused = i; std::printf(" attenuation %d -> %d\n", i + 1, rc); } + ++refused; + continue; + } + // The handle is of use while it is held: a name beneath it answers. + kal_node_info info = {}; + info.self_size = sizeof info; + check(kal_fs_info(held, "okw-attenuation-probe.tmp", 25, 0, KAL_INFO_KIND, &info) == kal_ok, "the attenuated directory answers"); + kal_fs_close_dir(held); + } + check(refused == 0, "256 attenuations, each closed before the next, are all made"); + + // And the table still bounds what is HELD at once: 64 trees, and the 65th is + // refused while they are; closing them gives every place back. + kal_dir many[80] = {}; + int made = 0; + for (int i = 0; i < 80; ++i) { + kal_dir_rights rights{}; + rights.self_size = sizeof rights; + rights.flags = KAL_DIR_READ_ONLY | KAL_DIR_UNCHANGING; + if (kal_fs_dir_attenuate(cwd, &rights, &many[made]) != kal_ok) break; + ++made; + } + std::printf(" held at once: %d\n", made); + check(made >= 1 && made < 80, "the trees held at once are bounded"); + for (int i = 0; i < made; ++i) kal_fs_close_dir(many[i]); + { + kal_dir_rights rights{}; + rights.self_size = sizeof rights; + rights.flags = KAL_DIR_READ_ONLY | KAL_DIR_UNCHANGING; + kal_dir again{}; + const int rc = kal_fs_dir_attenuate(cwd, &rights, &again); + check(rc == kal_ok, "a directory is attenuated again once the held ones are closed"); + if (rc == kal_ok) kal_fs_close_dir(again); + } + + std::printf("openkal-windows: a closed attenuation gives its place back\n"); +}}; diff --git a/tests/one_identity_for_a_node.cpp b/tests/one_identity_for_a_node.cpp new file mode 100644 index 0000000..392e4a8 --- /dev/null +++ b/tests/one_identity_for_a_node.cpp @@ -0,0 +1,117 @@ +// A regression test: a node has one identity, however it was reached. +// +// `kal_fs_info` (a name) and `kal_fs_file_info` (an open file) both report an +// identity of two words, the volume's and the file's. The first word came from +// two places: by handle, FileFsVolumeInformation's serial number, 32 bits; by +// name, where the system has the record of Windows 11 24H2 +// (FileStatBasicInformation), that record's serial number, the 64 bits the +// file system keeps. On builds 26100 and later the two words differed for +// every file: a file a program had opened and the same file asked about by +// name were two nodes. +// +// It surfaced three packages away. clangd keys the files it has entered by +// identity; its include analysis asked for a header by its resolved name, was +// answered with the other identity, found nothing and dereferenced the empty +// result (mcpp-language-server issue 86: the engine ended on every module unit +// that includes a library header, on Windows 11 24H2 and on nothing earlier). +// +// A SYSTEM WITHOUT THE RECORD CANNOT FAIL THIS. Windows Server 2022 (build +// 20348) answers a name by the older record or by opening, both of which use +// the handle's word, and wine has no record at all: the test reports there and +// proves nothing. The row that proves it is a 26100 build, which CI has. +#include +#include +#include +#include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; + +namespace { + +void check(bool ok, const char* what) { expect(ok) << what; } + +bool same(const kal_node_info& a, const kal_node_info& b) { + return a.identity[0] == b.identity[0] && a.identity[1] == b.identity[1]; +} + +} // namespace + +UTest one_identity_for_a_node_test{"one_identity_for_a_node", [] { + kal_dir cwd{}; + bool haveDir = false; + { + const kal_uintptr n = kal_fs_preopen_count(); + for (kal_uintptr i = 0; i < n && !haveDir; ++i) { + kal_dir d{}; + char nm[1024]; + kal_uintptr len = 0; + if (kal_fs_preopen(i, &d, nm, sizeof nm, &len) != kal_ok) continue; + cwd = d; + haveDir = true; + } + } + if (!haveDir) { + fail("a preopened directory to test in"); + return; + } + + static const char names[2][32] = { "okw-one-identity-a.tmp", "okw-one-identity-b.tmp" }; + kal_node_info opened[2] = {}; + for (int i = 0; i < 2; ++i) { + const kal_uintptr len = std::char_traits::length(names[i]); + kal_file f{}; + const int rc = kal_fs_open(cwd, names[i], len, KAL_OPEN_READ | KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE, &f); + check(rc == kal_ok, "a file is created"); + if (rc != kal_ok) return; + + opened[i].self_size = sizeof opened[i]; + check(kal_fs_file_info(f, KAL_INFO_ALL, &opened[i]) == kal_ok, "the open file answers"); + check((opened[i].present & KAL_INFO_IDENTITY) != 0u, "the open file has an identity"); + + // By name twice: the first enquiry of a volume may be answered by + // opening the node, and the ones after it by the record alone. While + // the file is open, and below once it is closed. + for (int round = 0; round < 2; ++round) { + kal_node_info named = {}; + named.self_size = sizeof named; + check(kal_fs_info(cwd, names[i], len, 0, KAL_INFO_ALL, &named) == kal_ok, "the name answers"); + check((named.present & KAL_INFO_IDENTITY) != 0u, "the name has an identity"); + std::printf(" %s: open %llx:%llx, by name (%d) %llx:%llx\n", names[i], + static_cast(opened[i].identity[0]), static_cast(opened[i].identity[1]), + round, static_cast(named.identity[0]), static_cast(named.identity[1])); + check(same(opened[i], named), "an open file is the node its name refers to"); + } + kal_fs_close_file(f); + + kal_node_info after = {}; + after.self_size = sizeof after; + check(kal_fs_info(cwd, names[i], len, KAL_FS_NO_RESOLVE, KAL_INFO_ALL, &after) == kal_ok, "the name answers, the file closed"); + check(same(opened[i], after), "the name is the same node after the file is closed"); + } + // And two files are two nodes: the identity is not a constant. + check(!same(opened[0], opened[1]), "two files have two identities"); + + // The directory itself, by a name of several components. + { + static const char dir[] = "okw-one-identity-d"; + kal_fs_mkdir(cwd, dir, sizeof dir - 1); // or it is there from a run that was interrupted + static const char inner[] = "okw-one-identity-d/inner.tmp"; + kal_file f{}; + const int rc = kal_fs_open(cwd, inner, sizeof inner - 1, KAL_OPEN_READ | KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE, &f); + check(rc == kal_ok, "a file is created beneath a directory"); + if (rc == kal_ok) { + kal_node_info o = {}, n = {}; + o.self_size = sizeof o; n.self_size = sizeof n; + check(kal_fs_file_info(f, KAL_INFO_ALL, &o) == kal_ok, "the open file answers"); + check(kal_fs_info(cwd, inner, sizeof inner - 1, 0, KAL_INFO_ALL, &n) == kal_ok, "its name of two components answers"); + check(same(o, n), "a name of several components is the node that was opened"); + kal_fs_close_file(f); + kal_fs_remove(cwd, inner, sizeof inner - 1); + } + kal_fs_remove(cwd, dir, sizeof dir - 1); + } + for (int i = 0; i < 2; ++i) kal_fs_remove(cwd, names[i], std::char_traits::length(names[i])); + + std::printf("openkal-windows: an open file is the node its name refers to\n"); +}}; From 33e3752dbdb265c54d9c0902b3b3c798a5817eb1 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 12 Nov 2026 18:21:41 +0900 Subject: [PATCH 2/2] 0.22.0: a node has one identity however it was reached; a closed attenuation gives its place back An identity's first word is the volume's serial number. A handle was asked FileFsVolumeInformation (32 bits); a name, where the system has Windows 11 24H2's FileStatBasicInformation, was given that record's serial number (the 64 bits the file system keeps). On builds 26100 and later an open file and its name were two nodes, and so were a listing's entry and an enquiry's answer: the specification's own case "what a listing gives of a file is what an enquiry gives" fails there, which the run on the commit before this one shows -- no row ran on that build until now. The handle's word is the identity's; the record's is translated to it through what is learned by opening one node of each volume. The rights table kept 64 roots for the life of the program and returned none: the 65th attenuation was refused whatever had been closed (the same run: "attenuation 65 -> 4" on every row). A root that keeps no record gives its place back at its last close. Found as mcpp-language-server issue 86. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 30 ++++++++++++++++++++- mcpp.toml | 2 +- src/fs.cpp | 72 +++++++++++++++++++++++++++++++++++++++++++++++--- src/rights.cpp | 27 ++++++++++++++----- 4 files changed, 119 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 75e0ca7..55dd9a7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. openkal = "0.25.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.21.0" +openkal-windows = "0.22.0" ``` Its purpose is as much to test the specification as to be used. openkal was @@ -15,6 +15,34 @@ drawn against systems that share an ancestry; a third implementation on a system that shares none of it is what turns "this interface is not Unix's shape" from a claim into an observation. +## What 0.22 corrects + +**A node has one identity, however it was reached.** The identity `kal_fs_info` +and `kal_fs_file_info` report is the volume's serial number and the file's index. +A handle's volume is asked `FileFsVolumeInformation`, whose serial number is 32 +bits; a name, where the system has Windows 11 24H2's `FileStatBasicInformation`, +was answered with that record's serial number, the 64 bits the file system keeps. +On builds 26100 and later a file that was open and the same file asked about by +name were therefore two nodes, and a program that keys a table of files by +identity held one file twice. The handle's word is the identity's, as it is on +every earlier system and in a listing; the record's word is translated to it +through what is learned by opening one node of each volume, so an enquiry by +name stays one request after the first of a volume. Nothing is assumed about how +the two numbers relate. (Found as mcpp-language-server issue 86: clangd as an ok +image ended in its include analysis on Windows 11 24H2 alone.) + +**A closed attenuation gives its place back.** `kal_fs_dir_attenuate` keeps 64 +trees for the life of the program. A tree's place was taken by every attenuation +and returned by none, so the 65th was refused with `kal_err_no_memory` however +many had been closed --- a program granting three read-only directories to each +program it starts was refused its 22nd start. A tree that keeps no record gives +its place back when its last handle closes; one that keeps a record holds it +until the program ends, which is when its record is ended. The 64 now bound the +trees held at once. + +CI gains a row on `windows-2025` (build 26100), the system the first of these +could be seen on. + ## What 0.21 adds (openkal 0.25) **A second name for a file** (`kal_fs_hard_link`) is the link record of diff --git a/mcpp.toml b/mcpp.toml index d92a959..4d0db09 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.21.0" +version = "0.22.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" diff --git a/src/fs.cpp b/src/fs.cpp index 06cdeb7..b22287d 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -890,10 +890,64 @@ namespace { volatile long long g_by_name = -1; // -1 not asked; 0 none; 1 the stat record; 2 the basic one too pfn_NtQueryInformationByName_ g_query_by_name = nullptr; +// ONE IDENTITY FOR A NODE, HOWEVER IT WAS REACHED (0.22.0). +// +// The identity's first word is the volume's serial number, and this system +// states that number twice. A handle is asked FileFsVolumeInformation, whose +// serial number is 32 bits; the record of Windows 11 24H2 (`volume_serial_number' +// above) is the 64 bits the file system keeps. Written into the identity as +// each came, the two differed for every file on such a system: a file opened +// and the same file asked about by name were two nodes. A caller that keys a +// table by identity --- a compiler's table of the files it has entered --- then +// held one file twice, and a lookup by name of a file it had opened missed +// (mcpp-language-server issue 86: clangd ended in its include analysis, on +// builds 26100 and 26200 and on no earlier one, which has not the record). +// +// The word a handle gives is the identity's, because it is what every system +// before 24H2 gave for a name too, and what a listing gives. How the 64 bits +// relate to the 32 is the file system's own affair and is not assumed here: a +// volume's two numbers are learned together, once, by opening a node of it --- +// the enquiry that was to be answered by name is answered by opening instead, +// as it is wherever the record is refused --- and every later enquiry of that +// volume is one request again. +struct volume_words { okw_u64 by_name; okw_u64 by_handle; }; +constexpr int kVolumes = 32; +volume_words g_volumes[kVolumes]; +int g_volume_count = 0; +SRWLOCK_ g_volume_lock{}; + +bool volume_of(okw_u64 by_name, okw_u64& by_handle) { + bool found = false; + AcquireSRWLockExclusive(&g_volume_lock); + for (int i = 0; i < g_volume_count; ++i) + if (g_volumes[i].by_name == by_name) { by_handle = g_volumes[i].by_handle; found = true; break; } + ReleaseSRWLockExclusive(&g_volume_lock); + return found; +} + +// A node of the volume was opened: its handle's word is the one the record's +// stands for. A table that is full learns nothing more, and the volumes beyond +// it are answered by opening every time, which is correct and slower. +void learn_volume(okw_u64 by_name, void* h) { + struct { okw::file_fs_volume_information info; wchar_t label_tail[128]; } volume{}; + okw::io_status_block s{}; + const long rv = okw::NtQueryVolumeInformationFile(h, &s, &volume, sizeof volume, + okw::fs_volume_information_class); + if (!(okw::ok(rv) || rv == okw::status_buffer_overflow)) return; + AcquireSRWLockExclusive(&g_volume_lock); + bool known = false; + for (int i = 0; i < g_volume_count; ++i) if (g_volumes[i].by_name == by_name) known = true; + if (!known && g_volume_count < kVolumes) + g_volumes[g_volume_count++] = { by_name, static_cast(volume.info.serial_number) }; + ReleaseSRWLockExclusive(&g_volume_lock); +} + // kal_ok or a kal_err when answered; 1 when the node is to be opened instead. +// `learn' is set, with the record's word for the volume, when the node is to +// be opened because that word's counterpart is not yet known. int info_by_name(void* root, const char* name, kal_uintptr len, kal_uintptr flags, kal_u32 wanted, - kal_node_info* out, bool& absent) { + kal_node_info* out, bool& absent, bool& learn, okw_u64& volume_by_name) { long long have = okw::atomic::load(&g_by_name); if (have < 0) { g_query_by_name = reinterpret_cast( @@ -942,8 +996,13 @@ int info_by_name(void* root, const char* name, kal_uintptr len, kal_uintptr flag // The volume's serial number, where the record did not give it. bool one_component = true; for (kal_uintptr i = 0; i < len; ++i) if (name[i] == '/' || name[i] == '\\') one_component = false; - okw_u64 serial = basic ? static_cast(b.volume_serial_number) : 0; - bool have_serial = basic; + okw_u64 serial = 0; + bool have_serial = false; + if (basic && (wanted & KAL_INFO_IDENTITY)) { + volume_by_name = static_cast(b.volume_serial_number); + if (!volume_of(volume_by_name, serial)) { learn = true; return 1; } + have_serial = true; + } if (!basic && (wanted & KAL_INFO_IDENTITY)) { if (!one_component) return 1; struct { okw::file_fs_volume_information info; wchar_t label_tail[128]; } volume{}; @@ -981,9 +1040,11 @@ int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, fill_absent(out); return kal_ok; } + bool learn = false; + okw_u64 volume_by_name = 0; { bool absent = false; - const int e = info_by_name(root, name, len, flags, wanted, out, absent); + const int e = info_by_name(root, name, len, flags, wanted, out, absent, learn, volume_by_name); if (e != 1) { if (absent) fill_absent(out); if (e == kal_ok && rights) asked(root, !absent, name, len); @@ -1013,6 +1074,9 @@ int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, return e; } if (rights) asked(root, true, name, len); + // The node the name ends at, opened as the record described it (a link was + // sent here before its volume was looked at): its volume is the record's. + if (learn) learn_volume(volume_by_name, h); const int e = fill(h, wanted, out); okw::NtClose(h); return e; diff --git a/src/rights.cpp b/src/rights.cpp index 6214a9a..2d1f039 100644 --- a/src/rights.cpp +++ b/src/rights.cpp @@ -398,12 +398,27 @@ void forget(void* h) { const int ri = e->root; drop_entry(e); if (ri >= 0 && --g_roots[ri].refs <= 0) { - // The last handle of the tree: what it recorded is written now. The - // root itself stays until the program ends, which writes its `e'. - flush(g_roots[ri]); - g_roots[ri].names.clear(); - g_roots[ri].listed.clear(); - if (g_roots[ri].base) { okw::NtClose(g_roots[ri].base); g_roots[ri].base = nullptr; } + // The last handle of the tree: what it recorded is written now. A root + // that keeps a record stays until the program ends, which writes its + // `e'. + root& r = g_roots[ri]; + flush(r); + r.names.clear(); + r.listed.clear(); + if (r.base) { okw::NtClose(r.base); r.base = nullptr; } + // A ROOT THAT KEEPS NO RECORD HAS NOTHING LEFT TO WRITE, AND ITS PLACE + // IS GIVEN BACK (0.22.0). The table has kRoots places for the life of + // the program, and a place was taken by every attenuation and returned + // by none: a program that grants the same three directories to each + // program it starts was refused its 22nd start (kal_err_no_memory, + // "out of memory" to its caller), with every earlier grant long closed + // (mcpp-language-server issue 86). + if (!r.record) { + give_back(r.rprefix, r.rplen + 1); + give_back(r.batch, kBatch); + r.seen.clear(); + r = root{}; + } } }