From faec29c78515896ec99d81969926811f78e1c107 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 17 Sep 2026 16:43:46 +0800 Subject: [PATCH 1/4] 0.8.0 --- openkal 0.13: whether a node may be started, and a start that is not a program kal_err_not_program maps ERROR_BAD_EXE_FORMAT and ERROR_EXE_MACHINE_TYPE_MISMATCH, this loader's two reports of a name that exists and is not a form it can start (ERROR_INVALID_EXE_SIGNATURE and ERROR_EXE_MARKED_INVALID, the same header- validation family, are mapped with them). Measured under Wine: CreateProcessW upon a text file with no recognised extension returns ERROR_BAD_EXE_FORMAT (193). kal_fs_set_executable_at is exported and does not claim KAL_FS_PROP_EXECUTABLE: this system's ordinary volumes do not record whether a node may be started, so the operation validates its arguments as its siblings do and reports kal_err_not_supported rather than simulate a property nothing here stores. kal_fs_info and kal_fs_file_info already left KAL_INFO_EXECUTABLE out of present, which needed no change. A spawn inherits only the handles it placed. CreateProcessW with inheritance enabled handed the started program every inheritable handle of this process, not only the three this operation named --- a defect independent of the specification, and the one a detached child's leaked standard output was found by: a program that kept the pipe open long after its starter had gone made whoever waited for the end of it wait for the wrong program. The fix is STARTUPINFOEXW with PROC_THREAD_ATTRIBUTE_HANDLE_LIST naming exactly the deduplicated, non-null standard handles this start placed; every other inheritable handle of the caller no longer crosses. tests/handle_inheritance.cpp proves it directly: an inheritable handle made for a reason of its own and never placed is not inherited, observed under Wine. The two directions of a connection are independent, clause 6.6. Every socket is now made with WSA_FLAG_OVERLAPPED; a synchronous handle on this system shares one completion event between whatever operations are issued upon it, so a read blocked in one context held back a write from another on the same connection, which is exactly what clause 6.6 forbids. kal_stream_read/write issue ReadFile/WriteFile with an OVERLAPPED of their own for a socket and wait for it synchronously; a file or a pipe, neither of which is overlapped, is unaffected. kal_datagram_send_to/recv_from do the same through WSASendTo/WSARecvFrom, so a datagram socket's two directions do not contend either. The conformance suite's new observation, that a write is not delayed by a read waiting on the same connection, holds. Comments carry no emoji. --- .github/workflows/ci.yml | 79 ++++++++++++++------- README.md | 37 ++++++++-- build.mcpp | 26 +++---- mcpp.toml | 8 +-- port/README.md | 8 +-- port/bcrypt.def | 4 +- port/kernel32.def | 5 ++ port/synchronization.def | 2 +- src/datagram.cpp | 71 ++++++++++++++----- src/endpoint.h | 51 +++++++++++--- src/env.cpp | 4 +- src/exec.cpp | 4 +- src/fs.cpp | 43 +++++++++--- src/memory.cpp | 2 +- src/net.cpp | 32 +++++---- src/process.cpp | 129 ++++++++++++++++++++++++++-------- src/random.cpp | 12 ++-- src/stream.cpp | 62 ++++++++++++++-- src/task.cpp | 4 +- src/timeout.cpp | 28 ++++---- src/version.cpp | 2 +- src/win.cpp | 16 ++++- src/win.h | 12 ++-- src/win32.h | 103 +++++++++++++++++++++------ tests/handle_inheritance.cpp | 132 +++++++++++++++++++++++++++++++++++ 25 files changed, 678 insertions(+), 198 deletions(-) create mode 100644 tests/handle_inheritance.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0e6ebf..e193235 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - name: Install mcpp run: | xlings update - # ⚠️ THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does + # THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does # not exist yet — that is the whole point of MCPP_SOURCE_REF. Bootstrap # from whatever the index has; the step below replaces it with the # build under review, and the pin is what an ordinary run tests. @@ -82,7 +82,7 @@ jobs: else xlings install "mcpp@$MCPP_VERSION" -y -g fi - # ⚠️⚠️ TRANSITION: GIVE THE BOOTSTRAP THE glibc ITS BINDING NAMES. + # TRANSITION: GIVE THE BOOTSTRAP THE glibc ITS BINDING NAMES. # # `xim:glibc`'s `latest` moved from `2.44` to `2.44.2`. A payload # directory is named after the version a request RESOLVED to, while a @@ -94,13 +94,13 @@ jobs: # error: selected RuntimeBinding glibc@2.44 requires payload # '…/xpkgs/xim-x-glibc/2.44', but it is not installed # - # ⚠️ On every NEW machine and on none that already existed, which is why + # On every NEW machine and on none that already existed, which is why # it is invisible from a developer's own. Measured on `main` as readily # as on any branch — the index records the same failure verbatim in # `pkgs/g/glibc.lua` and states the rule it broke: "The index is DATA # and the client is a PROGRAM: the consumer ships first." # - # ⭐ REMOVE THIS once a released mcpp resolves it. `mcpp 2026.8.27.1` + # REMOVE THIS once a released mcpp resolves it. `mcpp 2026.8.27.1` # accepts an installed payload whose version REFINES the requested one # (`payload_dir_for_version`), so a bootstrap from it needs nothing # here. Until then the missing payload is simply installed. @@ -112,13 +112,13 @@ jobs: fi mcpp --version mcpp self config --mirror GLOBAL - # ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. + # CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. # # Empty in the ordinary run, so this job keeps testing the RELEASED # mcpp the pin above names. Set it — `workflow_dispatch` input, or the # repository variable — and the same job runs against that source. # - # ⚠️ THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp + # THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp # releases went out green and only then turned this ecosystem red: the # engine's own CI cannot see a defect that appears only in a real # dependency graph, and this repository could not see the engine until @@ -131,7 +131,7 @@ jobs: [ -d "$src" ] || git clone --quiet --depth 1 \ --branch "$MCPP_SOURCE_REF" \ https://github.com/mcpp-community/mcpp.git "$src" - # ⚠️ THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp + # THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp # BUILDS IT. `.xlings.json` at mcpp's root pins the mcpp that # compiles mcpp, and that pin does not move when mcpp is released — # so a build inside the checkout obeys it and tries to install a @@ -144,7 +144,7 @@ jobs: # above, which is exactly what removing the file leaves. rm -f "$src/.xlings.json" ( cd "$src" && mcpp build --release ) - # ⚠️ BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and + # BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and # macOS runners too: on Windows the artefact is `mcpp.exe`, and # `-perm -u+x` is not a question that filesystem answers the way this # expects. Measured: `Finished release [optimized] in 173.44s` @@ -158,7 +158,7 @@ jobs: \( -name mcpp -o -name mcpp.exe \) | head -1) [ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; } echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" - # ⚠️ Reported, because a PATH entry that does not win looks exactly + # Reported, because a PATH entry that does not win looks exactly # like one that does until something built with the wrong engine. echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi @@ -170,7 +170,7 @@ jobs: # without a matching line in a `.def` compiles, and fails at the link of a # CONSUMER --- not of this package, which does not link. # - # ⚠️ MEASURED. Three declarations were added for openkal 0.8 and the names + # MEASURED. Three declarations were added for openkal 0.8 and the names # were not, and this package's own CI stayed green: the failure appeared in # openkal-llvm-runtime's cross-build, one repository away, as # @@ -182,14 +182,14 @@ jobs: run: | python3 - <<'PY' import glob, os, re, sys - # ⚠️ THE ENCODING IS NAMED. Python opens a file with the platform's + # THE ENCODING IS NAMED. Python opens a file with the platform's # default, which on this runner is cp1252, and these sources are UTF-8: # # UnicodeDecodeError: 'charmap' codec can't decode byte 0x90 # # A check that reads source files must say what they are encoded in, or # it reports on the runner's locale. - # ⚠️⚠️ EVERY HEADER, AND EVERY SPELLING. THIS CHECK MISSED THE SECOND + # 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' @@ -202,7 +202,7 @@ jobs: # ld.lld: error: undefined symbol: # __declspec(dllimport) NtQueryVolumeInformationFile # - # ⭐ THE CHECK WAS RIGHT AND ITS SCOPE WAS WRONG, which is the harder + # 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. @@ -248,7 +248,7 @@ jobs: run: | extra='' [ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}' - # ⚠️ THE SET IS ENUMERATED AND `optional` IS NOT NAMED, AND THE + # THE SET IS ENUMERATED AND `optional` IS NOT NAMED, AND THE # DIFFERENCE IS ONE INTERFACE. # # `optional` includes `space`, which this system does not provide: @@ -264,6 +264,37 @@ jobs: bash .spec/tools/run-conformance.sh openkal-windows . \ full,exec,random,terminal,net,datagram,timeout $extra + # run-conformance.sh above has already pointed the manifest at the + # specification's working tree and restored it through a trap; this + # package's own tests want the same working tree, so the substitution is + # repeated here rather than assumed to still hold. + - name: Point at the specification's working tree + run: | + set -euo pipefail + 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 + + # This package's own tests: today, the observation that a spawn does not + # leak a handle it did not place. A caller-made-inheritable handle never + # named in a spawn's streams must not reach the started program, and the + # test proves it directly rather than by inference from the conformance + # suite, which has no vocabulary for a handle outside openkal's own. + - name: This package's own tests + run: | + mcpp test 2>&1 | tee tests.log + missing=0 + for f in tests/*.cpp; do + name="$(basename "$f" .cpp)" + grep -q "^$name \.\.\. ok" tests.log || { echo "did not run or did not pass: $name" >&2; missing=1; } + done + test "$missing" -eq 0 + git checkout -- mcpp.toml + # --------------------------------------------------------------------------- # From a system that is not this one. # @@ -288,7 +319,7 @@ jobs: git -C .spec checkout --quiet "origin/$branch" fi - # ⚠️⚠️ CLONING THE SPECIFICATION IS NOT THE SAME AS BUILDING AGAINST IT, + # 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 @@ -302,7 +333,7 @@ jobs: # 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 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 @@ -311,7 +342,7 @@ jobs: - name: Point at the specification's working tree run: | set -euo pipefail - # ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and + # 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 @@ -348,7 +379,7 @@ jobs: - name: Install mcpp run: | xlings update - # ⚠️ THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does + # THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does # not exist yet — that is the whole point of MCPP_SOURCE_REF. Bootstrap # from whatever the index has; the step below replaces it with the # build under review, and the pin is what an ordinary run tests. @@ -358,13 +389,13 @@ jobs: xlings install "mcpp@$MCPP_VERSION" -y -g fi mcpp self config --mirror GLOBAL - # ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. + # CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. # # Empty in the ordinary run, so this job keeps testing the RELEASED # mcpp the pin above names. Set it — `workflow_dispatch` input, or the # repository variable — and the same job runs against that source. # - # ⚠️ THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp + # THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp # releases went out green and only then turned this ecosystem red: the # engine's own CI cannot see a defect that appears only in a real # dependency graph, and this repository could not see the engine until @@ -377,7 +408,7 @@ jobs: [ -d "$src" ] || git clone --quiet --depth 1 \ --branch "$MCPP_SOURCE_REF" \ https://github.com/mcpp-community/mcpp.git "$src" - # ⚠️ THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp + # THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp # BUILDS IT. `.xlings.json` at mcpp's root pins the mcpp that # compiles mcpp, and that pin does not move when mcpp is released — # so a build inside the checkout obeys it and tries to install a @@ -390,7 +421,7 @@ jobs: # above, which is exactly what removing the file leaves. rm -f "$src/.xlings.json" ( cd "$src" && mcpp build --release ) - # ⚠️ BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and + # BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and # macOS runners too: on Windows the artefact is `mcpp.exe`, and # `-perm -u+x` is not a question that filesystem answers the way this # expects. Measured: `Finished release [optimized] in 173.44s` @@ -404,7 +435,7 @@ jobs: \( -name mcpp -o -name mcpp.exe \) | head -1) [ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; } echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" - # ⚠️ Reported, because a PATH entry that does not win looks exactly + # Reported, because a PATH entry that does not win looks exactly # like one that does until something built with the wrong engine. echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi @@ -434,7 +465,7 @@ jobs: # this environment's would have its calls resolve to the program's, and # the program's would resolve back here. # - # ⚠️ IN BOTH PROFILES. An optimizing compiler turns loops into calls --- + # IN BOTH PROFILES. An optimizing compiler turns loops into calls --- # a counting loop into `strlen` or `wcslen` --- so objects built without # optimization can hold the property while the ones a release links do not. - name: The objects reference no C runtime symbol diff --git a/README.md b/README.md index 604cb98..948a9e0 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.9.0" +openkal = "0.13.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.5.0" +openkal-windows = "0.8.0" ``` Its purpose is as much to test the specification as to be used. openkal was @@ -117,11 +117,36 @@ The four that openkal 0.8 added and this implementation now provides: | | on this system | | --- | --- | -| `openkal.net` | Winsock, started once at the first socket and never stopped. ⚠️ `WSASocketW` with a flags word of zero rather than `socket`: the latter makes an **overlapped** handle, and `ReadFile` upon one of those returns before the bytes arrive. A non-overlapped socket is what lets a connection be a stream here with no second transfer path | -| `openkal.datagram` | the same calls with `SOCK_DGRAM`. ⚠️ This system reports a truncated message as a **failure** where the other two truncate silently; the bytes that fit are delivered either way, and the interface says the excess is lost | +| `openkal.net` | Winsock, started once at the first socket and never stopped. Every socket is made with `WSA_FLAG_OVERLAPPED`, and version 0.13 requires it: a synchronous socket shares one completion event between its two directions, so a read waiting in one thread held back a write from another on the same connection. The transfer operations issue `ReadFile`/`WriteFile` with an `OVERLAPPED` of their own and wait for it, so the two directions no longer contend for one event | +| `openkal.datagram` | the same calls with `SOCK_DGRAM`, `WSASendTo`/`WSARecvFrom` with their own `OVERLAPPED` for the same reason. This system reports a truncated message as a **failure** where the other two truncate silently; the bytes that fit are delivered either way, and the interface says the excess is lost | | `openkal.timeout` | `WSAPoll`, which answers for sockets and for nothing else. A bounded read of a stream that is not a socket reports `kal_err_not_supported` — which the interface's own header anticipates in terms. `kal_timeout_wait_process` is the one operation of the interface this system provides **directly**, because a bounded wait upon an object is the primitive here | | `openkal.exec` | `VirtualAlloc` writable, `VirtualProtect` executable, `FlushInstructionCache`. The third call is not optional and the other two systems' implementations do not need to make it explicit | +## Version 0.13 + +**A name that exists and is not a form this environment can start.** +`CreateProcessW` reports `ERROR_BAD_EXE_FORMAT` for an image whose header this +loader does not recognise and `ERROR_EXE_MACHINE_TYPE_MISMATCH` for one built for +a different processor; both arrive here as `kal_err_not_program` rather than +`kal_err_io`, which is what let a caller distinguish a name that is not there +from one that is and cannot be started. + +**Whether a node may be started is not recorded on this system's ordinary +volumes.** `kal_fs_set_executable_at` validates its arguments as its siblings do +and reports `kal_err_not_supported`; `KAL_FS_PROP_EXECUTABLE` is not claimed, and +`kal_fs_info`/`kal_fs_file_info` never set `KAL_INFO_EXECUTABLE`. Whether a +name may be started here is decided by its form when it is started, not read +from a property a volume stores. + +**A spawn inherits only the handles it placed.** `CreateProcessW` with +inheritance enabled hands the started program every inheritable handle of this +process, not only the ones a caller named — a defect distinct from the +specification, found by a detached child that kept a starter's standard output +open long after the starter had gone. The fix is `STARTUPINFOEXW` with a +`PROC_THREAD_ATTRIBUTE_HANDLE_LIST` naming exactly the deduplicated, non-null +standard handles this operation placed; a handle the caller made inheritable for +some other reason and did not place is no longer inherited. + ## Verification The conformance suite in the specification package, built for this target. The @@ -166,13 +191,13 @@ assumed — `WaitOnAddress` and its two neighbours are **not** in `kernel32.dll` and putting them there produces an import table that links and then fails to bind. -⚠️ **Supplied only where the system's own are absent.** On this system they are +**Supplied only where the system's own are absent.** On this system they are present, they are the vendor's, and they list every name rather than the forty-five this implementation calls; `-L` is searched first, so supplying ours there would shadow them and a consumer calling a forty-sixth would be told there is no such name. -⚠️ Measured 2026-08-22, on a clean continuous-integration runner, after every +Measured 2026-08-22, on a clean continuous-integration runner, after every object had compiled: ``` diff --git a/build.mcpp b/build.mcpp index 3f89376..338b66f 100644 --- a/build.mcpp +++ b/build.mcpp @@ -1,13 +1,13 @@ import mcpp; import std; -// ⭐⭐ WHERE THIS SYSTEM'S NAMES LIVE, PUT ON THE CONSUMER'S LINK LINE. +// WHERE THIS SYSTEM'S NAMES LIVE, PUT ON THE CONSUMER'S LINK LINE. // // `src/win32.h` and `src/win.h` freed the COMPILE from a vendor SDK. The LINK // was still reaching for `libkernel32.a` and its three neighbours — files that // exist on a machine with mingw installed and nowhere else. // -// ⚠️ Measured 2026-08-22 on a clean CI runner, after every object compiled: +// Measured 2026-08-22 on a clean CI runner, after every object compiled: // // lld: error: unable to find library -lkernel32 // lld: error: unable to find library -lntdll @@ -18,11 +18,11 @@ import std; // repository has recorded that shape before: a green that came from history the // new machine does not have. // -// ⭐ An import library is a LIST OF NAMES, not code — which is why generating +// An import library is a LIST OF NAMES, not code — which is why generating // one from `port/*.def` is a complete substitute rather than an approximation, // the same reason `openkal-macos/port/libSystem.tbd` is one for that system. // -// ⚠️⚠️ AND ONLY WHERE THE SYSTEM'S OWN ARE ABSENT. +// AND ONLY WHERE THE SYSTEM'S OWN ARE ABSENT. // // On this system they are present, they are the vendor's, and they list every // name rather than the forty-five this implementation calls. `-L` is searched @@ -34,9 +34,9 @@ import std; namespace { -// ⭐⭐ WHICH COMPILER FAMILY RESOLVED — AND WHAT AN ABSENT ANSWER MEANS. +// WHICH COMPILER FAMILY RESOLVED — AND WHAT AN ABSENT ANSWER MEANS. // -// ⚠️ READ FROM THE ENVIRONMENT, NOT THROUGH A HELPER. A build program is +// READ FROM THE ENVIRONMENT, NOT THROUGH A HELPER. A build program is // compiled against the `mcpp` module of whichever tool RUNS it, so naming a // helper newer than the released tool makes this package require an unreleased // one. Measured 2026-08-22, on every row of three repositories at once: @@ -48,12 +48,12 @@ namespace { // the FALLBACK written for the first. The environment variables are the // contract and `std::getenv` reads them on every version. // -// ⚠️ AND AN ABSENT VALUE IS NOT "NO COMPILER". Measured against mcpp 2026.8.19.1, +// AND AN ABSENT VALUE IS NOT "NO COMPILER". Measured against mcpp 2026.8.19.1, // which is what CI installs: `MCPP_TARGET_OS`, `MCPP_HOST`, `MCPP_OUT_DIR`, // `MCPP_MANIFEST_DIR`, `MCPP_TARGET` and `MCPP_TARGET_ARCH` are all set, and // `MCPP_COMPILER` and `MCPP_TOOLCHAIN_DIR` are absent. // -// ⭐ So absent means "a build tool from before the question could be asked" — +// So absent means "a build tool from before the question could be asked" — // and such a tool cannot produce the configuration the answer would change. The // clang-over-openkal cross to PE arrives on the same release as the variable. // The one configuration a tool that predates it can produce is the GCC one, so @@ -75,17 +75,17 @@ bool host_is_windows() { // The tool that turns a `.def` into an import library. // -// ⚠️ ITS NAME IS A PROPERTY OF THE COMPILER FAMILY, NOT OF THE PACKAGE. LLVM +// ITS NAME IS A PROPERTY OF THE COMPILER FAMILY, NOT OF THE PACKAGE. LLVM // spells it `llvm-dlltool`; binutils spells it `dlltool`, and a cross binutils // prefixes it with the triple. Measured 2026-08-22 — this file named only the // first, and the row of CI that builds with a GCC toolchain said: // // sh: 1: llvm-dlltool: not found // -// ⭐ So the family is ASKED for rather than assumed — see `compiler_family()` +// So the family is ASKED for rather than assumed — see `compiler_family()` // above for how, and why it is not `mcpp::compiler()`. // -// ⚠️ Asked for rather than DECLARED, too: a package that put `xim:llvm` in its +// Asked for rather than DECLARED, too: a package that put `xim:llvm` in its // dependencies would pin itself to one implementation of the toolchain. std::string dlltool() { const std::string dir = env_or_empty("MCPP_TOOLCHAIN_DIR"); @@ -120,7 +120,7 @@ int main() { if (host_is_windows()) return 0; - // ⭐⭐ AND ONLY UNDER CLANG, WHICH IS THE OTHER HALF OF THE SAME FACT. + // AND ONLY UNDER CLANG, WHICH IS THE OTHER HALF OF THE SAME FACT. // // A GCC toolchain for this format IS a mingw payload, and a mingw payload // carries these four libraries — complete ones, with every name rather than @@ -142,7 +142,7 @@ int main() { "bcrypt" }) { const auto def = std::format("{}/port/{}.def", root, name); const auto lib = std::format("{}/lib{}.a", out, name); - // ⚠️ `-m i386:x86-64` is stated. See port/README.md: the 32-bit ABI + // `-m i386:x86-64` is stated. See port/README.md: the 32-bit ABI // would need `@N` decoration on these names, which is a property of // that ABI rather than of the list. const auto cmd = std::format( diff --git a/mcpp.toml b/mcpp.toml index 1895a2f..46b6c24 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.7.4" +version = "0.8.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.12.0" +openkal = "0.13.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package @@ -38,7 +38,7 @@ openkal = "0.12.0" # the linker and the other as an input file. Naming them once in the spelling of # either would make this package build under two of the three toolchains it is # written for. -# ⚠️ THE PREDICATE IS THE OBJECT ABI, NOT THE C LIBRARY. +# THE PREDICATE IS THE OBJECT ABI, NOT THE C LIBRARY. # # These four are Win32 import libraries — a property of the platform interface # this package implements. `env = "gnu"` was standing in for "the GNU/PE ABI @@ -66,7 +66,7 @@ ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32", "-lbcrypt" # toolchains that reach it would not recognise these spellings, and openkal has # nothing to say about how a program that has a runtime unwinds. # -# ⚠️ AND NO LOOP IS REPLACED BY A CALL, WHICH IS WHAT `-fno-builtin` IS FOR. +# AND NO LOOP IS REPLACED BY A CALL, WHICH IS WHAT `-fno-builtin` IS FOR. # # An optimizing compiler recognises a loop that counts to a terminator and # emits a call to the library function that does the same. `wide_length` in diff --git a/port/README.md b/port/README.md index 32eb9d5..845705c 100644 --- a/port/README.md +++ b/port/README.md @@ -4,7 +4,7 @@ files say which library each of those names lives in, so that a link does not need a vendor SDK either. -⭐ **The same argument as the headers, applied one step later.** Removing +**The same argument as the headers, applied one step later.** Removing `` freed the COMPILE from a toolchain-provided SDK. The LINK was still reaching for `libkernel32.a` and its neighbours — files that exist on a machine with mingw installed and nowhere else. Measured 2026-08-22, on a clean @@ -17,11 +17,11 @@ lld: error: unable to find library -lshell32 lld: error: unable to find library -lsynchronization ``` -⚠️ **And it had passed on a developer's machine**, which had mingw installed — +**And it had passed on a developer's machine**, which had mingw installed — the shape this repository has recorded before as "a green that came from history the new machine does not have". -⭐ **An import library is a list of names, not code.** It carries the DLL each +**An import library is a list of names, not code.** It carries the DLL each name lives in and nothing else, which is why generating one from a list is a complete substitute rather than an approximation — the same reason `openkal-macos/port/libSystem.tbd` is one for that system. @@ -34,7 +34,7 @@ corresponding mingw import library rather than assumed: `WaitOnAddress` and its two neighbours are **not** in `kernel32.dll`, and putting them there would produce an import table that fails to bind on the system it names. -## ⚠️ x86-64 only, and the reason is stated rather than assumed +## x86-64 only, and the reason is stated rather than assumed `llvm-dlltool` is invoked with `-m i386:x86-64`. On the 32-bit ABI these names would need `@N` stdcall decoration, which is a property of that ABI rather than diff --git a/port/bcrypt.def b/port/bcrypt.def index 3f52272..de4edb1 100644 --- a/port/bcrypt.def +++ b/port/bcrypt.def @@ -1,10 +1,10 @@ ; The system's random generator, and nothing else from this library. ; -; ⭐ BCryptGenRandom WITH BCRYPT_USE_SYSTEM_PREFERRED_RNG, WHICH TAKES A NULL +; BCryptGenRandom WITH BCRYPT_USE_SYSTEM_PREFERRED_RNG, WHICH TAKES A NULL ; HANDLE. The ordinary use of this call opens an algorithm handle first, and a ; handle would give openkal.random an initialisation order it does not have. ; -; ⚠️ `ProcessPrng` (bcryptprimitives.dll) was tried first: fewer moving parts, +; `ProcessPrng` (bcryptprimitives.dll) was tried first: fewer moving parts, ; and what Chromium and Rust use. It fails on a WINDOWS HOST, where these ; generated libraries are not built at all --- `build.mcpp` returns immediately ; there, the vendor's own being present and complete --- and the Windows SDK diff --git a/port/kernel32.def b/port/kernel32.def index fe37633..5c6f080 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -6,10 +6,12 @@ LIBRARY KERNEL32.dll EXPORTS AcquireSRWLockExclusive CloseHandle +CreateEventW CreateFileW CreatePipe CreateProcessW CreateThread +DeleteProcThreadAttributeList FlushFileBuffers FlushInstructionCache FreeEnvironmentStringsW @@ -26,6 +28,7 @@ GetFinalPathNameByHandleW GetHandleInformation GetLastError GetLogicalDriveStringsW +GetOverlappedResult GetProcAddress GetProcessHeap GetStdHandle @@ -33,6 +36,7 @@ GetSystemInfo GetSystemTimePreciseAsFileTime HeapAlloc HeapFree +InitializeProcThreadAttributeList LoadLibraryW LocalFree MultiByteToWideChar @@ -51,6 +55,7 @@ CreateJobObjectW AssignProcessToJobObject TerminateJobObject SetConsoleCtrlHandler +UpdateProcThreadAttribute VirtualAlloc VirtualFree VirtualProtect diff --git a/port/synchronization.def b/port/synchronization.def index 6c9e4ba..0d59b3a 100644 --- a/port/synchronization.def +++ b/port/synchronization.def @@ -1,4 +1,4 @@ -; The suspension primitive. ⚠️ NOT IN kernel32.dll — these three live in +; The suspension primitive. NOT IN kernel32.dll — these three live in ; the API set below, and an import table naming the wrong library links and ; then fails to bind. Read out of mingw's own libsynchronization.a rather ; than assumed. diff --git a/src/datagram.cpp b/src/datagram.cpp index c150f04..f7b34d2 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -9,6 +9,12 @@ // datagram through it would lose the property that distinguishes this interface. // The packing is the same, the type is not, and the type is what prevents the // mistake. +// +// SENDING AND RECEIVING GO THROUGH `WSASendTo'/`WSARecvFrom' WITH AN +// `OVERLAPPED' OF THEIR OWN, VERSION 0.13, FOR THE REASON src/net.cpp STATES: +// the socket is made overlapped, so a synchronous call sharing the handle's one +// completion event would contend with a transfer in the other direction upon +// it. `kal_datagram_open' makes the socket with `WSA_FLAG_OVERLAPPED'. namespace { @@ -23,6 +29,36 @@ bool bad(SOCKET s) { return s == INVALID_SOCKET; } // statement about the type rather than a limit this implementation imposes. constexpr kal_uintptr kMaxOne = 0x7fffffffu; +// One overlapped operation upon the socket: an event of its own, issued and +// waited for synchronously. `err' carries the WSA error when this reports +// failure; a caller that must translate it uses okw::translate_wsa. +bool overlapped_once(SOCKET s, bool send, WSABUF_& wsabuf, DWORD flags, + void* addr, int* addrlen, DWORD* moved, int* err) { + auto* n = okw::net_or_null(); + if (n == nullptr) { *err = 0; return false; } + HANDLE ev = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (ev == nullptr) { *err = 0; return false; } + OVERLAPPED ov{}; + ov.hEvent = ev; + const int rc = send + ? n->send_to_ov(s, &wsabuf, 1, moved, flags, + addr, addrlen ? *addrlen : 0, &ov, nullptr) + : n->recv_from_ov(s, &wsabuf, 1, moved, &flags, + addr, addrlen, &ov, nullptr); + bool ok = rc == 0; + if (!ok) { + const int e = n->last_error(); + if (e == static_cast(ERROR_IO_PENDING)) { + ok = GetOverlappedResult(reinterpret_cast(s), &ov, moved, TRUE) != 0; + if (!ok) *err = static_cast(GetLastError()); + } else { + *err = e; + } + } + CloseHandle(ev); + return ok; +} + } // namespace extern "C" { @@ -42,7 +78,8 @@ int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) { auto* n = net(); if (n == nullptr) return kal_err_io; - const SOCKET s = n->socket(family, SOCK_DGRAM_, IPPROTO_UDP_, nullptr, 0, 0); + const SOCKET s = n->socket(family, SOCK_DGRAM_, IPPROTO_UDP_, nullptr, 0, + WSA_FLAG_OVERLAPPED_); if (bad(s)) return okw::last_socket_error(); if (local != nullptr) { @@ -83,16 +120,17 @@ kal_intptr kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len if (bad(s) || to == nullptr) return -kal_err_invalid; if (len > kMaxOne) return -kal_err_invalid; - auto* n = net(); - 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 -rc; - const int r = n->send_to(s, static_cast(buf), static_cast(len), - 0, &ss, addrlen); - if (r < 0) return -okw::last_socket_error(); + WSABUF_ wsabuf{ static_cast(len), + const_cast(static_cast(buf)) }; + DWORD sent = 0; + int err = 0; + if (!overlapped_once(s, true, wsabuf, 0, &ss, &addrlen, &sent, &err)) + return -okw::translate_wsa(err); // 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 @@ -100,8 +138,8 @@ kal_intptr kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len // it does not do. Reporting the short count as success would give a caller a // 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 == len ? static_cast(sent) : -kal_err_io; + const kal_uintptr moved = static_cast(sent); + return moved == len ? static_cast(moved) : -kal_err_io; } kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, @@ -110,15 +148,14 @@ kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, if (bad(s)) return -kal_err_invalid; if (len > kMaxOne) len = kMaxOne; - auto* n = net(); - if (n == nullptr) return -kal_err_io; ksockaddr_storage ss{}; int addrlen = static_cast(sizeof ss); - const int r = n->recv_from(s, static_cast(buf), static_cast(len), - 0, &ss, &addrlen); - if (r < 0) { - // ⚠️ THE ONE FAILURE THIS SYSTEM REPORTS THAT THE OTHER TWO DO NOT. + WSABUF_ wsabuf{ static_cast(len), static_cast(buf) }; + DWORD got = 0; + int err = 0; + if (!overlapped_once(s, false, wsabuf, 0, &ss, &addrlen, &got, &err)) { + // THE ONE FAILURE THIS SYSTEM REPORTS THAT THE OTHER TWO DO NOT. // // A message longer than the buffer is truncated here AND reported as a // failure --- `WSAEMSGSIZE' --- where the other two systems truncate @@ -131,7 +168,7 @@ kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, // // The count is not recoverable from this call, so what is reported is // the whole of the buffer, which is what was filled. - if (n->last_error() == okw::WSAEMSGSIZE) { + if (err == okw::WSAEMSGSIZE) { if (from != nullptr && okw::from_system(ss, *from) != kal_ok) { for (auto& b : from->addr) b = 0; from->addr_len = 0; @@ -142,7 +179,7 @@ kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, // caller may read. return static_cast(len); } - return -okw::last_socket_error(); + return -okw::translate_wsa(err); } if (from != nullptr) { @@ -155,7 +192,7 @@ kal_intptr kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, from->port = 0; } } - return static_cast(r); + return static_cast(got); } void kal_datagram_close(kal_datagram d) { diff --git a/src/endpoint.h b/src/endpoint.h index 7ad464c..7219dc9 100644 --- a/src/endpoint.h +++ b/src/endpoint.h @@ -14,7 +14,7 @@ namespace okw { // ── the network's own error values ────────────────────────────────────────── // -// ⚠️ A SECOND MAPPING, AND NOT AN ALTERNATIVE SPELLING OF THE FIRST. Every +// A SECOND MAPPING, AND NOT AN ALTERNATIVE SPELLING OF THE FIRST. Every // other call in this implementation reports through `GetLastError'; these // report through `WSAGetLastError', and the numbering does not overlap --- // every value below is ten thousand and something. Passing one of them to @@ -48,7 +48,7 @@ inline int translate_wsa(int e) { case WSAENETUNREACH: case WSAENETDOWN: return kal_err_not_found; case WSAEADDRINUSE: return kal_err_exists; case WSAEMFILE: return kal_err_no_space; - // ⚠️ NOT `kal_err_not_supported'. `WSANOTINITIALISED' means this + // NOT `kal_err_not_supported'. `WSANOTINITIALISED' means this // implementation failed to start the network interface, which is a // fault of this implementation and not an absence of the facility --- // reporting it as unsupported would tell a caller to stop asking. @@ -59,24 +59,24 @@ inline int translate_wsa(int e) { // ── reaching this system's network interface ──────────────────────────────── // -// ⚠️⚠️ RESOLVED AT RUN TIME RATHER THAN LINKED, AND src/win32.h RECORDS THE +// RESOLVED AT RUN TIME RATHER THAN LINKED, AND src/win32.h RECORDS THE // MEASUREMENT: this library's names are the BSD names, the C library above this // implementation defines the same names, and an import library puts both // definitions in one program. Nothing of ws2_32 enters this program's symbol // table now. // -// ⚠️ IT ALSO HAS TO BE STARTED. Every socket call fails with +// IT ALSO HAS TO BE STARTED. Every socket call fails with // `WSANOTINITIALISED' until `WSAStartup' has been called in this image, and // openkal has no operation a program calls first --- so the first operation // that needs the interface starts it. // -// ⭐ NOT A FUNCTION-LOCAL STATIC WITH A RUNTIME INITIALISER, AND THE MANIFEST +// NOT A FUNCTION-LOCAL STATIC WITH A RUNTIME INITIALISER, AND THE MANIFEST // SAYS WHY: every static in this package is initialised by a constant, so no // guard variable is emitted. One with a runtime initialiser would emit a call // to `__cxa_guard_acquire' --- a C runtime symbol, in the one package whose // continuous integration asserts it references none. // -// ⚠️ THE TABLE IS READ AND WRITTEN WITHOUT SYNCHRONISATION, AND THAT IS SAFE +// THE TABLE IS READ AND WRITTEN WITHOUT SYNCHRONISATION, AND THAT IS SAFE // HERE RATHER THAN OVERLOOKED. Two contexts racing resolve the same pointers // from the same library to the same values and perform a second `WSAStartup', // which this system reference-counts and documents as callable more than once. @@ -98,6 +98,9 @@ struct network_calls { pfn_sendto send_to; pfn_recvfrom recv_from; pfn_WSAPoll poll; + // Version 0.13: the overlapped forms, for a datagram's send and receive. + pfn_WSASendTo send_to_ov; + pfn_WSARecvFrom recv_from_ov; }; inline network_calls& net_calls() { @@ -105,7 +108,7 @@ inline network_calls& net_calls() { return c; } -// ⚠️ THE LIBRARY'S NAME IS WRITTEN AS WIDE CHARACTERS BY HAND. This package has +// THE LIBRARY'S NAME IS WRITTEN AS WIDE CHARACTERS BY HAND. This package has // no C library to take a literal converter from, and `L"ws2_32.dll"' is the // language's own; it is spelled out so that no header is needed for it. inline bool ensure_network() { @@ -132,14 +135,17 @@ inline bool ensure_network() { c.send_to = reinterpret_cast(at("sendto")); c.recv_from = reinterpret_cast(at("recvfrom")); c.poll = reinterpret_cast(at("WSAPoll")); + c.send_to_ov = reinterpret_cast(at("WSASendTo")); + c.recv_from_ov = reinterpret_cast(at("WSARecvFrom")); - // ⚠️ EVERY ONE OF THEM, OR NONE. A table with one null entry is worse than + // EVERY ONE OF THEM, OR NONE. A table with one null entry is worse than // no table: the operations that resolved would work and the one that did // not would call through zero, which is the failure clause 6.1 exists to // turn into a link error and this arrangement cannot. if (!start || !c.last_error || !c.socket || !c.close || !c.bind || !c.listen || !c.accept || !c.connect || !c.shutdown || !c.sockname || - !c.peername || !c.send_to || !c.recv_from || !c.poll) { + !c.peername || !c.send_to || !c.recv_from || !c.poll || + !c.send_to_ov || !c.recv_from_ov) { c.ready = -1; return false; } @@ -150,7 +156,7 @@ inline bool ensure_network() { return true; } -// The error this system last reported for a socket operation. ⚠️ Reached through +// The error this system last reported for a socket operation. Reached through // the table, so a caller that failed BEFORE the table was built --- which is the // only way `ensure_network' returns false --- is told `kal_err_io' rather than // calling through a null pointer. @@ -161,13 +167,36 @@ inline int last_socket_error() { } // The table, or a null pointer when this system's network interface could not -// be reached at all. ⚠️ Every operation of both interfaces begins here, so a +// be reached at all. Every operation of both interfaces begins here, so a // system without `ws2_32.dll' --- which is not a system this package expects to // meet --- reports `kal_err_io' rather than calling through zero. inline network_calls* net_or_null() { return ensure_network() ? &net_calls() : nullptr; } +// Whether a stream handle is a socket, version 0.13. GetFileType reports +// FILE_TYPE_PIPE for both a socket and an ordinary named pipe on this system, +// so the two cannot be told apart by that alone; getsockname succeeds only +// upon a socket, and every socket this implementation hands out has been +// connected, bound or accepted, so it always answers one. +// +// GetFileType IS ASKED FIRST. openkal.stream's transfer operations are called +// for every stream, not only a socket's, and the ordinary case — a file, the +// console, a pipe this implementation did not make into a socket — never +// touches the network interface at all. Asking getsockname first would load +// and start it (LoadLibraryW, WSAStartup) on every read or write of any +// stream; asking it only for a handle already reported as FILE_TYPE_PIPE +// confines that cost to what a socket and a real pipe share and nothing else +// does. +inline bool is_socket_handle(void* h) { + if (h == nullptr || GetFileType(h) != FILE_TYPE_PIPE) return false; + auto* n = net_or_null(); + if (n == nullptr) return false; + ksockaddr_storage ss{}; + int len = static_cast(sizeof ss); + return n->sockname(reinterpret_cast(h), &ss, &len) == 0; +} + // ── addresses ─────────────────────────────────────────────────────────────── // The port is carried in host order by kal_endpoint and in network order by the diff --git a/src/env.cpp b/src/env.cpp index 341fa79..ca96f19 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -54,7 +54,7 @@ void prepare() { if (parts) { for (int i = 0; i < count && g_argc < kMaxArgs - 1; ++i) { okw_uptr len = 0; - // ⚠️ AS GIVEN, AND UNTIL 0.7.3 EVERY BACKSLASH CAME OUT AS A SLASH. + // AS GIVEN, AND UNTIL 0.7.3 EVERY BACKSLASH CAME OUT AS A SLASH. // An argument is not a name: `C:\dir' and a pattern's `\d' are what // the caller wrote, and clause 7.6 requires the vector unaltered. g_argv[g_argc] = store(parts[i], wide_length(parts[i]), len); @@ -78,7 +78,7 @@ void prepare() { // bookkeeping and is not a variable a program set. if (p[0] != L'=') { okw_uptr total = 0; - // ⚠️⚠️ AS SET, AND UNTIL 0.7.4 EVERY BACKSLASH IN A VALUE CAME OUT AS + // AS SET, AND UNTIL 0.7.4 EVERY BACKSLASH IN A VALUE CAME OUT AS // A SLASH. A value is not a name any more than an argument is. // `ComSpec' is where programs find the command interpreter, and // a copy of this environment handed to a started program --- the diff --git a/src/exec.cpp b/src/exec.cpp index 3c17737..d9fa949 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -11,7 +11,7 @@ // on the other system. The interface states the narrower contract and this // implementation keeps to it. // -// ⭐ THE THIRD CALL IS NOT OPTIONAL AND IS NOT PRESENT ON THE OTHER TWO SYSTEMS' +// THE THIRD CALL IS NOT OPTIONAL AND IS NOT PRESENT ON THE OTHER TWO SYSTEMS' // IMPLEMENTATIONS. A processor whose instruction path does not observe the data // path's writes must be told; this system publishes an operation for exactly // that and documents it as required after writing code into memory. On the @@ -54,7 +54,7 @@ int kal_exec_publish(void* p, kal_uintptr size) { void kal_exec_free(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return; - // ⚠️ THE SIZE IS ZERO AND THAT IS NOT AN OVERSIGHT. This system's release + // THE SIZE IS ZERO AND THAT IS NOT AN OVERSIGHT. This system's release // takes a size of zero with `MEM_RELEASE' and refuses any other value: the // region released is the whole of the one that was reserved, which is what // this operation means. Passing the caller's size would fail with an diff --git a/src/fs.cpp b/src/fs.cpp index 5cf0794..b067d0f 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -166,14 +166,14 @@ int fill(void* h, kal_u32 wanted, kal_node_info* out) { : kal_node_file; v.writable = (basic.attributes & FILE_ATTRIBUTE_READONLY) ? 0 : 1; - // ⭐ THE IDENTITY IS TWO WORDS BECAUSE ONE IS NOT ENOUGH, AND THIS + // 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. - // ⚠️⚠️ THE VOLUME ENQUIRY REPORTS AN OVERFLOW AND ANSWERS ANYWAY, AND + // 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 @@ -183,7 +183,7 @@ int fill(void* h, kal_u32 wanted, kal_node_info* out) { // 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 + // 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 @@ -417,7 +417,7 @@ int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { // The modification time of a NAME, including a directory. Version 0.10. // -// ⚠️ AND THE OPEN IS NOT `kal_fs_open''S. That one names `FILE_NON_DIRECTORY_FILE' +// AND THE OPEN IS NOT `kal_fs_open''S. That one names `FILE_NON_DIRECTORY_FILE' // --- correctly, since it opens a FILE --- and a directory is exactly what this // declaration exists to reach. Opening for the attribute alone also means a // caller need not be able to write the contents to stamp them, which is what @@ -443,13 +443,34 @@ int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, return okw::ok(w) ? kal_ok : okw::translate_nt(w); } +// Whether a NAME's node may be started, version 0.13. +// +// Not claimed here. This system decides whether an image may be started from +// its form at the moment it is started (kal_process_spawn, through +// CreateProcessW) rather than from a property a volume records; NTFS and the +// FAT family have no such record, and inventing one here would be a +// simulation clause 3.1 forbids rather than an answer this environment gives. +// KAL_FS_PROP_EXECUTABLE is therefore never in kal_fs_props, and every valid +// request is refused with kal_err_not_supported, which is what the header +// states for an implementation that does not claim the position. +// +// The arguments are still validated: an invalid directory or an unacceptable +// name is kal_err_invalid, exactly as the operations beside this one refuse +// them, before the volume is asked whether it could perform the rest. +int kal_fs_set_executable_at(kal_dir base, const char* name, kal_uintptr len, + int) { + void* root = dir_handle(base); + if (!root || !okw::acceptable(name, len)) return kal_err_invalid; + return kal_err_not_supported; +} + // --- exclusion upon a range of a file --------------------------------------- // -// ⭐ THIS SYSTEM EXCLUDES PER HANDLE, WHICH IS WHAT openkal STATES. The other +// THIS SYSTEM EXCLUDES PER HANDLE, WHICH IS WHAT openkal STATES. The other // two kernels carry an older form held by the PROCESS and have to reach past it; // here there is nothing to reach past. // -// ⚠️ AND THIS SYSTEM'S EXCLUSION IS MANDATORY RATHER THAN ADVISORY: a write that +// AND THIS SYSTEM'S EXCLUSION IS MANDATORY RATHER THAN ADVISORY: a write that // crosses a locked range is refused by the system, where elsewhere it is refused // only to a program that asked. That is a difference a caller can observe, and // it is the environment's own; nothing here can or should simulate the weaker @@ -489,7 +510,7 @@ int kal_fs_unlock(kal_file f, kal_u64 start, kal_u64 len) { // How much the volume holds, in bytes. // -// ⚠️ `available' AND NOT `total free'. This system reports the units this +// `available' AND NOT `total free'. This system reports the units this // CALLER may use, which is the question openkal asks; a quota makes the two // differ and the larger of them is not an answer a program can act upon. int kal_fs_capacity(kal_dir d, kal_u64* total, kal_u64* available) { @@ -659,7 +680,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, // may be otherwise --- and a word per implementation could state neither. // Whether the environment beneath actually performs a lock. // -// ⚠️ ASKED ON A DIRECTORY, WHICH IS NOT A THING THIS SYSTEM LOCKS --- and that is +// ASKED ON A DIRECTORY, WHICH IS NOT A THING THIS SYSTEM LOCKS --- and that is // what makes the question answerable without disturbing anything. A system that // implements the operation refuses a directory as a wrong request; one that has // not implemented it says so with a different value, and that difference is the @@ -692,7 +713,7 @@ static bool locking_available() { kal_uintptr kal_fs_props(kal_dir d) { void* h = dir_handle(d); - // ⚠️⚠️ LOCKING IS ASKED ABOUT RATHER THAN ASSUMED, AND THE REASON IS NOT + // LOCKING IS ASKED ABOUT RATHER THAN ASSUMED, AND THE REASON IS NOT // THE VOLUME. // // This system locks a byte range, and the three continuous-integration rows @@ -700,7 +721,7 @@ kal_uintptr kal_fs_props(kal_dir d) { // the result under an emulator of this system --- which EXPORTS the call and // answers `STATUS_NOT_IMPLEMENTED' when it is made. // - // ⭐ So the property is not a property of the volume here, nor of the + // So the property is not a property of the volume here, nor of the // format: it is a property of what is beneath the program at the moment it // asks. A word that claimed the position regardless would be describing the // INTERFACE rather than the environment --- and the whole purpose of a @@ -721,7 +742,7 @@ kal_uintptr kal_fs_props(kal_dir d) { 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 + // 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 diff --git a/src/memory.cpp b/src/memory.cpp index b25c212..879bbfd 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -64,7 +64,7 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { // The quantum this environment allocates and protects memory in. // -// ⭐⭐ THIS SYSTEM HAS TWO, AND THE COARSER IS REPORTED. It 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 diff --git a/src/net.cpp b/src/net.cpp index 2bcaca9..f6ed49d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -10,14 +10,19 @@ // it yields is the socket itself, because a socket IS a handle on this system // and openkal.stream's operations here are `ReadFile' and `WriteFile'. // -// ⭐⭐ THAT LAST SENTENCE IS THE WHOLE REASON THIS IMPLEMENTATION NEEDS NO -// SECOND TRANSFER PATH, AND IT IS TRUE ONLY BECAUSE OF THE FLAGS WORD PASSED TO -// `WSASocketW'. The ordinary `socket' makes an OVERLAPPED handle, whose reads -// and writes complete asynchronously; `ReadFile' upon one of those returns -// before the bytes have arrived. A flags word of zero makes a socket that is -// not overlapped, and the two calls then behave exactly as they do upon a pipe. -// This is stated here rather than left in the argument list because a change to -// that one zero would not fail to compile and would not fail to link. +// THE SOCKET IS MADE OVERLAPPED (WSA_FLAG_OVERLAPPED), AND VERSION 0.13 +// REQUIRES IT. A synchronous handle on this system has one completion event +// shared by every operation upon it; two synchronous transfers issued from two +// contexts contend for that one event regardless of which direction each is +// in, so a read blocked waiting for the peer held back a write the peer was +// waiting for on the same connection --- clause 6.6 forbids exactly that. +// `ReadFile' and `WriteFile' upon the overlapped socket therefore each carry an +// `OVERLAPPED' of their own (src/stream.cpp), so the two directions no longer +// share anything to contend over. An earlier revision made the socket +// non-overlapped instead, on the reasoning that it would then behave exactly as +// a pipe does upon `ReadFile'; measured against two contexts and a bound, a +// write on one waited for a read blocked on the other, which is the connection +// clause 6.6 exists to rule out. namespace { @@ -30,7 +35,8 @@ okw::network_calls* net() { return okw::net_or_null(); } SOCKET make(int family, int type, int protocol) { auto* n = net(); - return n ? n->socket(family, type, protocol, nullptr, 0, 0) : INVALID_SOCKET; + return n ? n->socket(family, type, protocol, nullptr, 0, WSA_FLAG_OVERLAPPED_) + : INVALID_SOCKET; } int report_address(bool peer, SOCKET s, kal_endpoint* out) { @@ -88,7 +94,7 @@ int kal_net_listen(const kal_endpoint* local, kal_net_listener* out) { const SOCKET s = make(family, SOCK_STREAM_, IPPROTO_TCP_); if (bad(s)) return okw::last_socket_error(); - // ⚠️ SO_REUSEADDR DOES NOT MEAN HERE WHAT IT MEANS ON THE OTHER TWO SYSTEMS, + // SO_REUSEADDR DOES NOT MEAN HERE WHAT IT MEANS ON THE OTHER TWO SYSTEMS, // AND THAT IS WHY IT IS NOT SET. // // There it permits a listener whose predecessor is lingering. Here it @@ -131,9 +137,9 @@ int kal_net_accept(kal_net_listener l, kal_net_conn* out) { const SOCKET c = n->accept(s, nullptr, nullptr); if (bad(c)) return okw::last_socket_error(); - // ⚠️ A CONNECTION INHERITS THE LISTENER'S PROPERTIES AND NOT ITS FLAGS WORD. - // The listener was made non-overlapped; an accepted connection is - // non-overlapped too, which is what keeps `ReadFile' synchronous upon it. + // A CONNECTION INHERITS THE LISTENER'S PROPERTIES. The listener is made + // overlapped, and an accepted connection is overlapped too --- which is what + // src/stream.cpp's transfer path upon it relies upon. out->h = okw::pack_socket(c); if (out->h == 0) { n->close(c); return kal_err_no_memory; } return kal_ok; diff --git a/src/process.cpp b/src/process.cpp index 3ccdf55..67b9847 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -22,7 +22,7 @@ namespace { constexpr okw_uptr kCommandLine = 32768; // this environment's own bound -// ⚠️ THE NAMES A STARTED PROGRAM IS GIVEN, IN THE FORM EVERY PROGRAM READS. +// THE NAMES A STARTED PROGRAM IS GIVEN, IN THE FORM EVERY PROGRAM READS. // // GetFinalPathNameByHandleW answers with the `\\?\' prefix, which tells this // system to take the rest verbatim. CreateProcessW accepts it, and the started @@ -51,18 +51,26 @@ okw_uptr plain_name(wchar_t* s, okw_uptr n) { return n - drop; } -// ⚠️ THE HANDLES A START PLACES ARE INHERITABLE FOR THE LENGTH OF THE START AND NO -// LONGER, AND ONE START AT A TIME DOES THIS. +// A SPAWN INHERITS ONLY THE HANDLES IT PLACED. Version 0.13. // // `CreateProcessW' with inheritance enabled gives the started program EVERY -// inheritable handle of this process, not only the three in its start-up record. -// A handle left marked after a start reaches the next program started, and a -// handle marked by a start on another context reaches this one; either way a -// program ends up holding a pipe that belongs to another, and whoever waits for -// the end of that pipe waits for the wrong program. So each placed handle is -// marked, the program is started, and each is put back as it was --- a borrowed -// standard stream is the caller's and not this operation's to change --- with a -// lock around the whole of it. +// inheritable handle of this process, not only the three in its start-up +// record --- and that was once the whole of what this implementation relied +// upon: a caller-inheritable handle the caller had not placed here crossed the +// spawn anyway, because bInheritHandles does not discriminate. A detached +// child then kept a starter's own standard output open long after the starter +// had gone, and whoever waited for the end of that pipe waited for the wrong +// program to end. `PROC_THREAD_ATTRIBUTE_HANDLE_LIST' is the position this +// system offers for exactly that: it narrows what `CreateProcessW' inherits to +// the array named there, and a handle outside it is not inherited even when it +// is itself marked inheritable for some reason of the caller's own. +// +// A handle in that array must still be marked inheritable, which is the half +// this file already did: each placed handle is marked, the program is +// started, and each is put back as it was --- a borrowed standard stream is +// the caller's and not this operation's to change --- with a lock around the +// whole of it, because marking is a property of the handle and not of the +// call. SRWLOCK_ g_starting{}; struct inheritance { @@ -70,6 +78,12 @@ struct inheritance { DWORD before[3]{}; bool marked[3]{}; bool held = false; + // The handles this start actually places, deduplicated: what goes into + // PROC_THREAD_ATTRIBUTE_HANDLE_LIST. The system refuses a list holding one + // handle twice, which stdout and stderr placed upon the same pipe would + // otherwise be. + HANDLE list[3]{}; + DWORD count = 0; inheritance(bool active, HANDLE in, HANDLE out, HANDLE err) { if (!active) return; @@ -80,7 +94,7 @@ struct inheritance { handle[i] = given[i]; if (given[i] == nullptr || given[i] == INVALID_HANDLE_VALUE) continue; // One stream placed twice --- output and error, typically --- is - // marked and put back once. + // marked and put back once, and named once in the list below. bool again = false; for (int j = 0; j < i; ++j) again = again || (marked[j] && handle[j] == given[i]); if (again) continue; @@ -88,6 +102,12 @@ struct inheritance { if (!GetHandleInformation(given[i], &flags)) continue; before[i] = flags & HANDLE_FLAG_INHERIT; marked[i] = SetHandleInformation(given[i], HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) != 0; + // A handle this system would not make inheritable --- a console + // pseudo-handle is the ordinary case --- is not named here either: + // naming an uninheritable handle in the explicit list is refused by + // the call that builds it, where the old, unrestricted inheritance + // simply left such a handle uninherited. + if (marked[i]) list[count++] = given[i]; } } ~inheritance() { @@ -97,6 +117,39 @@ struct inheritance { } }; +// The attribute list naming `window.list' to `CreateProcessW'. Obtained and +// released around one call, the way `scratch' below is: this system sizes the +// buffer for a caller, so there is a first call that only measures. +struct attribute_list { + void* buffer = nullptr; + unsigned long long size = 0; + LPPROC_THREAD_ATTRIBUTE_LIST list = nullptr; + + bool build(const inheritance& window) { + if (window.count == 0) return false; + InitializeProcThreadAttributeList(nullptr, 1, 0, &size); + buffer = kal_alloc(static_cast(size), alignof(void*)); + if (buffer == nullptr) return false; + list = static_cast(buffer); + if (!InitializeProcThreadAttributeList(list, 1, 0, &size)) { list = nullptr; return false; } + // `window.list' outlives this call --- it is the caller's local, held + // until CreateProcessW returns --- so nothing here copies it again. + if (!UpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + const_cast(window.list), + static_cast(window.count) * sizeof(HANDLE), + nullptr, nullptr)) { + DeleteProcThreadAttributeList(list); + list = nullptr; + return false; + } + return true; + } + ~attribute_list() { + if (list) DeleteProcThreadAttributeList(list); + if (buffer) kal_free(buffer, static_cast(size), alignof(void*)); + } +}; + bool append_wide(wchar_t* out, okw_uptr cap, okw_uptr& at, const wchar_t* s, okw_uptr n) { if (at + n + 1 >= cap) return false; for (okw_uptr i = 0; i < n; ++i) out[at++] = s[i]; @@ -129,7 +182,7 @@ bool append_utf8(wchar_t* out, okw_uptr cap, okw_uptr& at, const char* s, okw_up return true; } -// ⚠️⚠️ UNTIL 0.7.3 EVERY SEPARATOR WAS DROPPED, AND EVERY ELEMENT WAS QUOTED. +// UNTIL 0.7.3 EVERY SEPARATOR WAS DROPPED, AND EVERY ELEMENT WAS QUOTED. // // A run of backslashes was counted and never written, so `C:\dir\file' arrived as // `C:dirfile' and a trailing one ended the quoting early and joined the elements @@ -168,7 +221,7 @@ extern "C" { // Starting a program. One function since openkal 0.11. // -// ⚠️ TWO POSITIONS IN `kal_spawn' ARE REFUSED HERE, AND EACH REFUSAL +// TWO POSITIONS IN `kal_spawn' ARE REFUSED HERE, AND EACH REFUSAL // IS OLDER THAN 0.11 --- the record moved, the answers did not. // // `grants': this environment has no numbering a preopen could arrive under, so @@ -179,7 +232,7 @@ extern "C" { // // `KAL_SPAWN_BOUND_LIFETIME': no primitive arms it from inside the started image. // -// ⭐⭐ AND THE UNIT IS IMPLEMENTED HERE, WHICH AN EARLIER SHAPE OF IT WAS NOT. +// AND THE UNIT IS IMPLEMENTED HERE, WHICH AN EARLIER SHAPE OF IT WAS NOT. // // 0.11 first spelled this as a flag: make the started program a unit, and let // `kal_process_terminate' reach the unit afterwards. That shape could not be @@ -189,7 +242,7 @@ extern "C" { // because `getpgid(pid) == pid' recovers it from the kernel; this one would have // needed a registry. // -// ⚠️ Clause 7.1 states mechanically what needing a registry means: the +// Clause 7.1 states mechanically what needing a registry means: the // specification "has taken a shape borrowed from one environment, and THE SHAPE // IS AT FAULT rather than the implementation". handle.h says the same one level // down --- its array "holds generations and nothing else", and a lookup deciding @@ -201,7 +254,7 @@ extern "C" { // object is created and its handle reported; where the unit is a process group // the first member's identifier is reported instead. // -// ⚠️ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE IS DELIBERATELY NOT SET. It would make +// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE IS DELIBERATELY NOT SET. It would make // `kal_process_job_close' end every member --- and closing means only releasing // where the unit is a number, so one operation would mean two things. Ending is // `kal_process_job_terminate' and nothing else is. @@ -243,7 +296,7 @@ int kal_process_spawn(const kal_spawn* how, // between execution contexts would make two concurrent spawns one. struct scratch { wchar_t image[okw::kMaxName]; - // ⭐ THE DIRECTORY THE PROGRAM RUNS IN, WHICH IS NOT THE ONE IT IS NAMED + // THE DIRECTORY THE PROGRAM RUNS IN, WHICH IS NOT THE ONE IT IS NAMED // FROM. `CreateProcessW' has taken a current directory all along; what // was missing until 0.11 was a caller able to say which. wchar_t cwd[okw::kMaxName]; @@ -315,8 +368,9 @@ int kal_process_spawn(const kal_spawn* how, } block[block_used++] = 0; - STARTUPINFOW startup{}; - startup.cb = sizeof startup; + STARTUPINFOEXW ex{}; + STARTUPINFOW& startup = ex.startup; + startup.cb = sizeof(STARTUPINFOW); bool inherit = false; if (streams && (streams->in.h || streams->out.h || streams->err.h)) { startup.dwFlags = STARTF_USESTDHANDLES; @@ -334,9 +388,24 @@ int kal_process_spawn(const kal_spawn* how, DWORD refusal = 0; { inheritance window(inherit, startup.hStdInput, startup.hStdOutput, startup.hStdError); + + // ONLY THE HANDLES `window' NAMED ARE ASKED TO CROSS. Without an + // explicit list, bInheritHandles=TRUE would still hand the started + // program every inheritable handle of this process, so it is set only + // together with the list that narrows what that means; a request with + // nothing markable inherits nothing rather than everything. + attribute_list attrs; + DWORD creation = CREATE_UNICODE_ENVIRONMENT; + BOOL inherit_handles = FALSE; + if (window.count > 0 && attrs.build(window)) { + startup.cb = sizeof(STARTUPINFOEXW); + ex.lpAttributeList = attrs.list; + creation |= EXTENDED_STARTUPINFO_PRESENT; + inherit_handles = TRUE; + } + started = CreateProcessW(image, argc ? line : nullptr, nullptr, nullptr, - inherit ? TRUE : FALSE, - CREATE_UNICODE_ENVIRONMENT, + inherit_handles, creation, envc ? block : nullptr, cwd, &startup, &info); // Read before the handles are put back, which may set it again. if (!started) refusal = GetLastError(); @@ -344,7 +413,7 @@ int kal_process_spawn(const kal_spawn* how, if (!started) return okw::translate_win32(refusal); CloseHandle(info.hThread); - // ⚠️ ASSIGNED BEFORE THE CALLER IS TOLD ANYTHING. A program that could not be + // ASSIGNED BEFORE THE CALLER IS TOLD ANYTHING. A program that could not be // put into the unit is not the program that was asked for --- it would outlive // a termination of the unit --- so it is ended rather than handed back. if (unit && !AssignProcessToJobObject(unit, info.hProcess)) { @@ -361,9 +430,9 @@ int kal_process_spawn(const kal_spawn* how, return kal_ok; } -// ⭐⭐ A WORD THIS ENVIRONMENT SETS WHEN SOMEBODY HAS ASKED THIS PROGRAM TO END. +// A WORD THIS ENVIRONMENT SETS WHEN SOMEBODY HAS ASKED THIS PROGRAM TO END. // -// ⚠️ AND THIS IS WHY THE INTERFACE IS A WORD RATHER THAN A HANDLER. The +// AND THIS IS WHY THE INTERFACE IS A WORD RATHER THAN A HANDLER. The // notification here arrives ON A CONTEXT OF ITS OWN --- the environment starts one // to run the routine --- which is nothing like a disposition interrupting whatever // was running. An interface shaped like the other system's signals would have @@ -383,14 +452,14 @@ BOOL OKW_API stop_routine(DWORD) { } } // namespace -// ⚠️ Armed on the first enquiry, so that adding this operation changes nothing +// Armed on the first enquiry, so that adding this operation changes nothing // for a program that does not use it. const kal_u32* kal_process_stop_requested(void) { if (!g_stop_armed) { g_stop_armed = 1; SetConsoleCtrlHandler(stop_routine, TRUE); } return &g_stop_word; } -// This program itself joins or forms a unit. ⭐ NATURAL HERE TOO, and by the +// This program itself joins or forms a unit. NATURAL HERE TOO, and by the // route this environment already offers: a job object is created before it has // members, so the caller simply becomes its first one. int kal_process_job_enter(kal_job* j) { @@ -423,7 +492,7 @@ int kal_process_job_terminate(kal_job j) { return kal_ok; } -// ⚠️ RELEASES AND DOES NOT END. The limit that would have ended the members on +// RELEASES AND DOES NOT END. The limit that would have ended the members on // the last close is deliberately not set --- see the note above kal_process_spawn. void kal_process_job_close(kal_job j) { HANDLE h = okw::unpack(j.h); @@ -432,7 +501,7 @@ void kal_process_job_close(kal_job j) { // A channel: a pair of streams of which one end is meant to cross a spawn. // -// ⚠️⚠️ NEITHER END IS INHERITABLE, AND UNTIL 0.7.2 THE FAR ONE WAS FROM THE MOMENT +// NEITHER END IS INHERITABLE, AND UNTIL 0.7.2 THE FAR ONE WAS FROM THE MOMENT // IT WAS CREATED. // // This environment decides inheritance per handle, and a start with inheritance @@ -514,7 +583,7 @@ void kal_process_close(kal_process p) { if (h) { okw::retire(p.h); CloseHandle(h); } } -// ⚠️ THREE POSITIONS ARE DELIBERATELY ABSENT, AND EACH IS ABSENT BECAUSE THE +// THREE POSITIONS ARE DELIBERATELY ABSENT, AND EACH IS ABSENT BECAUSE THE // NEXT CALL REFUSES IT. A word claiming a facility the operation then declines is // the disagreement clause 6.2 exists to prevent, so the two are written together // and read together: diff --git a/src/random.cpp b/src/random.cpp index 7f9d547..ec84a0d 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -1,6 +1,6 @@ // openkal.random on Windows --- BCryptGenRandom with the system-preferred RNG. // -// ⭐ THE FLAG IS WHAT REMOVES THE HANDLE, AND THE HANDLE WAS THE WHOLE OBJECTION. +// THE FLAG IS WHAT REMOVES THE HANDLE, AND THE HANDLE WAS THE WHOLE OBJECTION. // // `BCryptGenRandom` ordinarily takes an algorithm handle, which this backend // would have to open on every call or cache — and caching one gives this @@ -8,7 +8,7 @@ // `BCRYPT_USE_SYSTEM_PREFERRED_RNG` says "use the system's own generator" and // takes a null handle, which is exactly what is wanted here. // -// ⚠️ AND NOT `ProcessPrng`, WHICH WAS TRIED FIRST AND FAILS ON A WINDOWS HOST. +// AND NOT `ProcessPrng`, WHICH WAS TRIED FIRST AND FAILS ON A WINDOWS HOST. // // lld-link: error: undefined symbol: __declspec(dllimport) ProcessPrng // @@ -20,12 +20,12 @@ // Windows SDK ships `bcrypt.lib` but no import library for // `bcryptprimitives.dll`. `ProcessPrng` is documented and has no `.lib`. // -// ⭐ A backend that links on one host and not another is not a backend. The +// A backend that links on one host and not another is not a backend. The // name below is exported by `bcrypt.dll` and listed by `bcrypt.lib`, so it // resolves through the vendor's libraries and through this package's generated // one alike. // -// ⚠️ THE FIRST DIAGNOSIS OF THIS WAS WRONG AND IS RECORDED SO IT IS NOT REPEATED: +// THE FIRST DIAGNOSIS OF THIS WAS WRONG AND IS RECORDED SO IT IS NOT REPEATED: // it read `/usr/x86_64-w64-mingw32/lib` and concluded from mingw's contents. // mingw is not part of this ecosystem — it is the very thing `build.mcpp` // exists to stop depending on, as the note at the top of that file says. What @@ -36,7 +36,7 @@ // them because a directive travels in the object that carries it, and the // object a linker pulls in for `kal_random_fill` is this one. // -// ⚠️ TWO SPELLINGS IN THE MANIFEST WERE TRIED BEFORE THIS AND BOTH WERE PUSHED, +// TWO SPELLINGS IN THE MANIFEST WERE TRIED BEFORE THIS AND BOTH WERE PUSHED, // while the mechanism was already established in this package and named four // lines below the block being edited. `ldflags` reaches the command line // verbatim, and the two toolchains that `env = "msvc"` selects reject each @@ -71,7 +71,7 @@ int kal_random_fill(void* out, kal_uintptr len) { // rest of this file spells things: the header it lives in is the system's. constexpr unsigned long use_system_preferred_rng = 0x00000002ul; - // ⚠️ NO LOOP. This call fills the whole buffer or fails; unlike a read it + // NO LOOP. This call fills the whole buffer or fails; unlike a read it // has no short return, so a loop would always run once and would suggest a // partial state this interface does not have. const long st = BCryptGenRandom(nullptr, static_cast(out), diff --git a/src/stream.cpp b/src/stream.cpp index b62b0c2..3bfd118 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -1,12 +1,50 @@ #include "win.h" +#include "endpoint.h" #include +// The transfer operations of openkal.stream. A file, a pipe and a socket all +// arrive here as one HANDLE-shaped word; every one of them reads and writes +// through `ReadFile'/`WriteFile', and a socket differs only in how the one +// transfer is carried out --- see below and src/net.cpp's note on why a socket +// is overlapped at all, version 0.13. + namespace { void* handle_of(kal_stream s) { return reinterpret_cast(s.h); } bool valid(void* h) { return h != nullptr && h != INVALID_HANDLE_VALUE; } +// One transfer upon an overlapped handle: an event of its own, issued and +// waited for synchronously, so that this call's completion does not depend on +// or contend with another transfer in the other direction upon the same +// handle. `kal_stream_read'/`kal_stream_write' remain synchronous from their +// caller's point of view; only the mechanism underneath changes. +// +// A count short of what was asked is a correct report of an overlapped +// transfer exactly as it is of a synchronous one --- this is one call, not the +// loop `kal_stream_write' performs to satisfy clause 7.4. +bool overlapped_once(void* h, void* buf, DWORD want, bool write, + DWORD* moved, DWORD* err) { + HANDLE ev = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (ev == nullptr) { *err = GetLastError(); return false; } + OVERLAPPED ov{}; + ov.hEvent = ev; + const BOOL immediate = write ? WriteFile(h, buf, want, moved, &ov) + : ReadFile(h, buf, want, moved, &ov); + bool ok = immediate != 0; + if (!ok) { + const DWORD e = GetLastError(); + if (e == ERROR_IO_PENDING) { + ok = GetOverlappedResult(h, &ov, moved, TRUE) != 0; + if (!ok) *err = GetLastError(); + } else { + *err = e; + } + } + CloseHandle(ev); + return ok; +} + } // namespace extern "C" { @@ -21,6 +59,7 @@ kal_stream kal_stderr(void) { return kal_stream{ reinterpret_cast(G kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr len) { void* h = handle_of(s); if (!valid(h)) return -kal_err_invalid; + const bool socket = okw::is_socket_handle(h); const auto* p = static_cast(buf); kal_uintptr done = 0; while (done < len) { @@ -31,9 +70,15 @@ kal_intptr 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)) { + DWORD err = 0; + const bool ok = socket + ? overlapped_once(h, const_cast(p + done), chunk, + true, &written, &err) + : WriteFile(h, p + done, chunk, &written, nullptr) != 0; + if (!ok) { + if (!socket) err = GetLastError(); if (done != 0) return static_cast(done); - return -okw::translate_win32(GetLastError()); + return -okw::translate_win32(err); } if (written == 0) break; done += written; @@ -44,15 +89,20 @@ kal_intptr kal_stream_write(kal_stream s, const 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 -kal_err_invalid; + const bool socket = okw::is_socket_handle(h); const DWORD want = len > 0x7fffffffu ? 0x7fffffffu : static_cast(len); DWORD got = 0; - if (!ReadFile(h, buf, want, &got, nullptr)) { - const unsigned long e = GetLastError(); + DWORD err = 0; + const bool ok = socket + ? overlapped_once(h, buf, want, false, &got, &err) + : ReadFile(h, buf, want, &got, nullptr) != 0; + if (!ok) { + if (!socket) err = GetLastError(); // 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; - return -okw::translate_win32(e); + if (err == ERROR_BROKEN_PIPE || err == ERROR_HANDLE_EOF) return 0; + return -okw::translate_win32(err); } // 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. diff --git a/src/task.cpp b/src/task.cpp index 0f8de64..57e45c7 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -104,12 +104,12 @@ int kal_task_wait(const kal_u32* word, kal_u32 expected, // How many contexts can run at the same moment. Version 0.10. // -// ⚠️ Added because its absence was a WRONG ANSWER and not a refusal: the +// Added because its absence was a WRONG ANSWER and not a refusal: the // property word says whether contexts run at once and not how many can, so a C // library above answered 1 with no error and a program sizing a pool of workers // got one worker. // -// ⭐ THE ACTIVE MASK AND NOT THE COUNT FIELD. This record carries both, and they +// THE ACTIVE MASK AND NOT THE COUNT FIELD. This record carries both, and they // differ whenever a program is confined to part of the machine --- which is the // case a program sizing itself most needs to get right. kal_uintptr kal_task_parallelism(void) { diff --git a/src/timeout.cpp b/src/timeout.cpp index 0692694..5ddb6a8 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -9,7 +9,7 @@ // socket would transfer without blocking, so a bounded read is a bounded wait // for readiness followed by the ordinary read. // -// ⚠️⚠️ AND NO SINGLE CALL ANSWERS FOR EVERY RESOURCE HERE, WHICH IS THE ONE +// AND NO SINGLE CALL ANSWERS FOR EVERY RESOURCE HERE, WHICH IS THE ONE // PLACE THIS SYSTEM DIFFERS FROM THE OTHER TWO IN KIND RATHER THAN IN SPELLING. // // There, one call answers for every descriptor. Here a socket, a pipe and a @@ -18,14 +18,14 @@ // one does not wait. Three enquiries, one per kind, chosen by asking what the // handle is. // -// ⭐ AND THE PIPE IS NOT OPTIONAL. `openkal.process' makes a channel out of a +// AND THE PIPE IS NOT OPTIONAL. `openkal.process' makes a channel out of a // pipe here, so a C library above this implementation reaches `poll' and // `select' upon one --- and a `select' that reported `kal_err_not_supported' // for a pipe would make every program that waits on a subprocess's output stop. // Measured: openkal-musl's own network probe, on the row that builds for this // system, reported `select reports the read end ready (errno=38)'. // -// ⚠️ A SOCKET ALSO REPORTS `FILE_TYPE_PIPE', so the socket enquiry is made +// A SOCKET ALSO REPORTS `FILE_TYPE_PIPE', so the socket enquiry is made // FIRST and the file type only decides what a non-socket is. // // ⇒ What remains unbounded is a character device --- a console --- and @@ -39,7 +39,7 @@ namespace { // A duration of zero denotes no bound, which is the convention kal_task_wait // establishes. `WSAPoll' expresses that with a negative number. // -// ⚠️ A BOUND SHORTER THAN A MILLISECOND ROUNDS UP TO ONE AND NOT DOWN TO NONE. +// A BOUND SHORTER THAN A MILLISECOND ROUNDS UP TO ONE AND NOT DOWN TO NONE. // Rounding down would turn a wait into a poll, and the header is explicit: a // caller that asks for less is not refused and does not get less. int bound_ms(kal_u64 ns) { @@ -53,7 +53,7 @@ int bound_ms(kal_u64 ns) { // Whether this word names a socket, which is the first question because a // socket also reports `FILE_TYPE_PIPE'. // -// ⚠️ THE TEST IS THAT `getsockname' SUCCEEDS, AND NOT THAT IT FAILED FOR SOME +// THE TEST IS THAT `getsockname' SUCCEEDS, AND NOT THAT IT FAILED FOR SOME // PARTICULAR REASON. The first form of this function read "it is a socket // unless the failure was WSAENOTSOCK", which makes the answer depend on which // error a system chooses for a handle that is not one --- and Wine does not @@ -87,7 +87,7 @@ int await(SOCKET s, short events, kal_u64 ns) { if (n == nullptr) return kal_err_not_supported; WSAPOLLFD_ p{ s, events, 0 }; const int r = n->poll(&p, 1, bound_ms(ns)); - // ⭐ THE REAL ERROR IS KEPT HERE AND NARROWED IN `await_stream'. This + // THE REAL ERROR IS KEPT HERE AND NARROWED IN `await_stream'. This // function is reached with a socket this implementation made --- from // `kal_timeout_accept' and `kal_timeout_recv_from', where the resource is // known --- so a failure carries information a caller can act upon. It is @@ -103,11 +103,11 @@ int await(SOCKET s, short events, kal_u64 ns) { // Waits for a pipe to have bytes, without taking them. // -// ⭐ `PeekNamedPipe' IS THE ONE NON-DESTRUCTIVE READINESS ENQUIRY IN THIS WHOLE +// `PeekNamedPipe' IS THE ONE NON-DESTRUCTIVE READINESS ENQUIRY IN THIS WHOLE // ECOSYSTEM, and it is why this implementation needs no read-ahead where the // port above it does. It reports how many bytes are there and takes none. // -// ⚠️ A CLOSED WRITING END IS READY AND NOT AN ERROR. The call then fails with +// A CLOSED WRITING END IS READY AND NOT AN ERROR. The call then fails with // `ERROR_BROKEN_PIPE', and a read that follows reports the end of input without // waiting --- which is what readiness asserts. Reporting the failure here would // make a program that reads until end-of-input wait for ever instead. @@ -119,7 +119,7 @@ int await_pipe(HANDLE h, kal_u64 ns) { if (!PeekNamedPipe(h, nullptr, 0, nullptr, &available, nullptr)) { const DWORD e = GetLastError(); if (e == ERROR_BROKEN_PIPE || e == ERROR_PIPE_NOT_CONNECTED) return kal_ok; - // ⚠️ A FAILURE OF THE ENQUIRY IS NOT AN ERROR OF THE TRANSFER, and + // A FAILURE OF THE ENQUIRY IS NOT AN ERROR OF THE TRANSFER, and // reporting it as one would put this operation's answer outside the // set the interface defines for it. What this call could not do is // BOUND the operation; the transfer that follows reports whatever @@ -136,7 +136,7 @@ int await_pipe(HANDLE h, kal_u64 ns) { } } -// ⭐⭐ EVERY PATH OUT OF THIS FUNCTION IS ONE OF THREE: kal_ok, kal_err_again, +// EVERY PATH OUT OF THIS FUNCTION IS ONE OF THREE: kal_ok, kal_err_again, // kal_err_not_supported. // // That is the set `openkal.timeout' defines for the WAIT it adds, and keeping @@ -144,21 +144,21 @@ int await_pipe(HANDLE h, kal_u64 ns) { // An error belonging to the RESOURCE --- an invalid handle, a reset connection // --- is the transfer's to report, and the transfer follows this call. // -// ⚠️ MEASURED TWICE, BOTH TIMES AS THE SAME SHAPE. An earlier form returned +// MEASURED TWICE, BOTH TIMES AS THE SAME SHAPE. An earlier form returned // `kal_err_invalid' for a handle of zero; a later one returned whatever // `PeekNamedPipe' or `WSAPoll' had failed with. The conformance suite reported // both as "a bounded read reports success, an expiry, or a refusal" not // holding, and the second time only under Wine --- which is to say, only where // the system chose a different error for the same condition. int await_stream(kal_stream s, short events, kal_u64 ns) { - // ⚠️ ONE REASON TO REFUSE, AND NOT TWO. An earlier form answered a null or + // ONE REASON TO REFUSE, AND NOT TWO. An earlier form answered a null or // invalid handle with `kal_err_invalid' and everything else with // `kal_err_not_supported', and the conformance suite reported both bounded // reads of the standard input as not holding: a run whose standard input is // not attached has a handle of zero, and the suite's list of admissible // answers is the interface's --- success, an expiry, or a refusal. // - // ⭐ The early return was answering a DIFFERENT QUESTION. "Is this handle + // The early return was answering a DIFFERENT QUESTION. "Is this handle // valid" is what the unbounded operation answers; what this interface // answers is whether this implementation can bound an operation upon this // resource. @@ -226,7 +226,7 @@ int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* termin void* h = okw::unpack(p.h); if (h == nullptr) return kal_err_invalid; - // ⭐ THE ONE OPERATION OF THIS INTERFACE THIS SYSTEM PROVIDES DIRECTLY. The + // THE ONE OPERATION OF THIS INTERFACE THIS SYSTEM PROVIDES DIRECTLY. The // other two poll a child in a loop because neither has a bounded wait for // one; here waiting upon an object with a bound IS the primitive, and the // bound is stated in the same milliseconds `WSAPoll' takes. diff --git a/src/version.cpp b/src/version.cpp index 7e4febf..49a2e3e 100644 --- a/src/version.cpp +++ b/src/version.cpp @@ -8,7 +8,7 @@ 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 + // `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. diff --git a/src/win.cpp b/src/win.cpp index f2dfae0..a6760dd 100644 --- a/src/win.cpp +++ b/src/win.cpp @@ -55,7 +55,19 @@ int translate_win32(unsigned long e) { case ERROR_ALREADY_EXISTS: return kal_err_exists; case ERROR_DIR_NOT_EMPTY: return kal_err_not_empty; case ERROR_DIRECTORY: return kal_err_not_directory; - // ⚠️⚠️ A LOCK THAT ANOTHER HOLDER HAS IS `AGAIN' AND NOT AN + // A name that exists and is not a form this loader can start. Version + // 0.13, kal_err_not_program. Measured under Wine: CreateProcessW upon a + // text file with no recognised extension reports ERROR_BAD_EXE_FORMAT. + // ERROR_EXE_MACHINE_TYPE_MISMATCH is this loader's documented report for + // an image built for a different processor, which is the same condition + // under a different cause; ERROR_EXE_MARKED_INVALID and + // ERROR_INVALID_EXE_SIGNATURE belong to the same header-validation + // family and are mapped with it rather than left to fall to kal_err_io. + case ERROR_INVALID_EXE_SIGNATURE: + case ERROR_EXE_MARKED_INVALID: + case ERROR_BAD_EXE_FORMAT: + case ERROR_EXE_MACHINE_TYPE_MISMATCH: return kal_err_not_program; + // A LOCK THAT ANOTHER HOLDER HAS IS `AGAIN' AND NOT AN // INPUT-OUTPUT FAILURE, and this line is missing from every earlier // release because nothing here took a lock until openkal 0.10. // @@ -65,7 +77,7 @@ int translate_win32(unsigned long e) { // device rather than a conflict with another holder --- and a caller // reading that would stop rather than retry. // - // ⭐ It is distinct from ERROR_SHARING_VIOLATION above, which stays + // It is distinct from ERROR_SHARING_VIOLATION above, which stays // `permission': that one is a conflict over how a file was OPENED and // is not resolved by asking again. case ERROR_LOCK_VIOLATION: return kal_err_again; diff --git a/src/win.h b/src/win.h index dd7d59f..beb6d93 100644 --- a/src/win.h +++ b/src/win.h @@ -26,13 +26,13 @@ // one layer down, there is nothing to resolve. #pragma once -// ⭐ THIS SYSTEM'S INTERFACE, DECLARED BY THIS PACKAGE. +// THIS SYSTEM'S INTERFACE, DECLARED BY THIS PACKAGE. // // It used to be `` and `` — a vendor SDK that had to be // found somewhere, and "somewhere" turned out to be whichever copy the machine // happened to have. win32.h records the measurement and the method. // -// ⚠️ AND THE ARGUMENT IS THE ONE THIS FILE ALREADY MAKES, ten lines down, about +// AND THE ARGUMENT IS THE ONE THIS FILE ALREADY MAKES, ten lines down, about // ``. That argument was always the same argument; it had only been // applied to the half where one TOOLCHAIN disagrees with another, and not to // the half where one MACHINE disagrees with another. @@ -188,7 +188,7 @@ enum : unsigned long { }; extern "C" { -// ⚠️ `dllimport` HERE TOO, AND IT IS THE SAME FACT AS IN `win32.h`. +// `dllimport` HERE TOO, AND IT IS THE SAME FACT AS IN `win32.h`. // // These live in `ntdll.dll`. Omitting the attribute still links — the linker // synthesises a thunk through the import table — but it changes what the OBJECT @@ -222,7 +222,7 @@ __declspec(dllimport) long __stdcall NtQueryDirectoryFile(void* handle, void* ev unsigned char restart); __declspec(dllimport) long __stdcall NtFlushBuffersFile(void* handle, io_status_block* status); -// ⭐ EXCLUSION IS PER-HANDLE ON THIS SYSTEM, which is exactly what openkal +// EXCLUSION IS PER-HANDLE ON THIS SYSTEM, which is exactly what openkal // states: the holder is the `kal_file'. There is no second, process-held form // to avoid here --- the thing the other two kernels have to reach past. __declspec(dllimport) long __stdcall NtLockFile(void* handle, void* event, void* apc, void* apc_context, @@ -236,7 +236,7 @@ __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 +// 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 @@ -245,7 +245,7 @@ inline bool ok(long status) { return status >= 0; } // reported to have the same identity. inline constexpr long status_buffer_overflow = static_cast(0x80000005ul); -// ⚠️ AN ENVIRONMENT MAY EXPORT A NAME AND NOT IMPLEMENT WHAT IT NAMES, and the +// AN ENVIRONMENT MAY EXPORT A NAME AND NOT IMPLEMENT WHAT IT NAMES, and the // two are distinguishable only by this value. It is what a capability word has // to consult before claiming a position: an operation whose export resolves and // whose call answers this cannot be performed here, and a word claiming it would diff --git a/src/win32.h b/src/win32.h index bfc60e9..3fb0f2d 100644 --- a/src/win32.h +++ b/src/win32.h @@ -1,6 +1,6 @@ // The whole of what this implementation uses from this system's own interface. // -// ⭐⭐ WHY THIS FILE EXISTS, AND WHY IT IS NOT A DEVIATION. +// WHY THIS FILE EXISTS, AND WHY IT IS NOT A DEVIATION. // // openkal is a specification. It says nothing about how a backend is // implemented, and the four that exist do it four ways: openkal-linux issues @@ -15,7 +15,7 @@ // machine has. On a machine with neither, the build fails naming a header // rather than a missing dependency. // -// ⚠️ Measured 2026-08-23, cross-compiling this package with the target side +// Measured 2026-08-23, cross-compiling this package with the target side // coming from packages rather than from a payload: // // win.h:35 → /usr/x86_64-w64-mingw32/include/windows.h @@ -27,12 +27,12 @@ // succeed against different declarations, which is the shape of every false // green this repository has recorded. // -// ⭐ AND THE OTHER THREE IMPLEMENTATIONS ALREADY SHOW THE ANSWER. None of them +// AND THE OTHER THREE IMPLEMENTATIONS ALREADY SHOW THE ANSWER. None of them // takes a vendor SDK: openkal-linux writes the system-call numbers, openkal- // opensbi writes the SBI extension identifiers, openkal-macos writes a stub // listing the two names it borrows. This file is that, for this system. // -// ⚠️ AND HALF OF IT WAS ALREADY WRITTEN. `win.h` beside this file has carried +// AND HALF OF IT WAS ALREADY WRITTEN. `win.h` beside this file has carried // the entire NT object-manager layer since it was written, with the reason // stated: `` "is present in one toolchain's sources and partial in // another's". That argument was always the same argument; it had only been @@ -41,7 +41,7 @@ // // HOW THE LIST WAS OBTAINED // -// ⚠️ Not by reading the sources. `` was removed and the compiler was +// Not by reading the sources. `` was removed and the compiler was // asked what it then could not resolve; the answer is this file. That is the // same method openkal-macos used for its stub, and it is preferred for the same // reason: a reading produces names the configured build never uses, and the @@ -50,13 +50,13 @@ // ── the machine's own words ───────────────────────────────────────────────── // -// ⚠️ Written out rather than taken from a C library. This package is built by +// Written out rather than taken from a C library. This package is built by // three toolchains and this file must mean the same thing under all of them, // and ``'s own spellings are ultimately these. using BOOL = int; using BYTE = unsigned char; using WORD = unsigned short; -using DWORD = unsigned long; // ⚠️ `long`, not `int` — this is LLP64 +using DWORD = unsigned long; // `long`, not `int` — this is LLP64 using UINT = unsigned int; using ULONG = unsigned long; using LONG = long; @@ -69,12 +69,12 @@ using LPCWSTR = const wchar_t*; using LPSTR = char*; using LPCSTR = const char*; -// The calling convention. ⚠️ It is ignored on the 64-bit ABI and load-bearing on +// The calling convention. It is ignored on the 64-bit ABI and load-bearing on // the 32-bit one, and writing it costs nothing on either — while omitting it // would make this file wrong for a target it is otherwise correct for. #define OKW_API __stdcall -// ⭐⭐ AND WHERE THEY LIVE, WHICH IS NOT AN OPTIMISATION. +// AND WHERE THEY LIVE, WHICH IS NOT AN OPTIMISATION. // // Every function below is in a DLL, and `` says so with // `__declspec(dllimport)`. Omitting it still LINKS: the linker notices the @@ -82,7 +82,7 @@ using LPCSTR = const char*; // through the import table. The program runs, so nothing here fails — and that // is exactly why it has to be written rather than discovered. // -// ⚠️ Measured 2026-08-23. This package's own independence check permits +// Measured 2026-08-23. This package's own independence check permits // `__imp_*` because those names ARE this environment's interface reached // through its import table, and it rejects everything else because everything // else would be a C runtime. Declaring these without `dllimport` made the @@ -120,7 +120,7 @@ struct OVERLAPPED { HANDLE hEvent; }; -// ⚠️ THE LAYOUT IS THE CONTRACT. These two are filled in by this package and +// THE LAYOUT IS THE CONTRACT. These two are filled in by this package and // read by the system, so a member of the wrong width does not fail to compile — // it shifts everything after it. The order is the documented one. struct STARTUPINFOW { @@ -142,8 +142,20 @@ struct PROCESS_INFORMATION { DWORD dwThreadId; }; +// The extended form, version 0.13: a STARTUPINFOW with an attribute list +// attached, so that a start can narrow which handles are inherited to a list +// this operation names rather than every inheritable handle of the caller. The +// first member is a STARTUPINFOW so that a pointer to this may be passed where +// CreateProcessW's declaration below still names STARTUPINFOW — the layout +// agrees with `cb' and `EXTENDED_STARTUPINFO_PRESENT' the way the system +// documents it. +struct STARTUPINFOEXW { + STARTUPINFOW startup; + LPVOID lpAttributeList; +}; + // ── the constants this package names ──────────────────────────────────────── -// ⚠️ NOT `constexpr`. A cast from an integer to a pointer is not a constant +// NOT `constexpr`. A cast from an integer to a pointer is not a constant // expression, and the compiler says so — `` spells this as a macro // for the same reason. `inline const` gives one object across every translation // unit without claiming something the language does not allow. @@ -165,7 +177,7 @@ enum : DWORD { FILE_SHARE_READ = 0x1, FILE_SHARE_WRITE = 0x2, FILE_SHARE_DELETE = 0x4, // The access rights this package asks for, and the two composites the - // system documents. ⚠️ A composite written as its own number rather than + // system documents. A composite written as its own number rather than // assembled here: assembling it would be this file deciding what the system // means by "generic read", and it does not get to decide that. FILE_READ_DATA = 0x0001, @@ -196,6 +208,13 @@ enum : DWORD { HANDLE_FLAG_INHERIT = 0x1, STARTF_USESTDHANDLES = 0x00000100u, + // For kal_process_spawn: the explicit handle-inheritance list, version + // 0.13. `EXTENDED_STARTUPINFO_PRESENT' tells CreateProcessW that + // `STARTUPINFOEXW.lpAttributeList' is populated; the attribute number is the + // one this system documents for `PROC_THREAD_ATTRIBUTE_HANDLE_LIST'. + EXTENDED_STARTUPINFO_PRESENT = 0x00080000u, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002u, + INFINITE = 0xFFFFFFFFu, WAIT_OBJECT_0 = 0x00000000u, WAIT_TIMEOUT_ = 0x00000102u, @@ -204,7 +223,7 @@ enum : DWORD { MB_ERR_INVALID_CHARS = 0x8, }; -// The error values this package translates. ⚠️ Only these — openkal's error set +// The error values this package translates. Only these — openkal's error set // is closed, and a value with no mapping is reported as `kal_err_io` rather than // invented, so listing more would be listing names nothing reads. enum : DWORD { @@ -223,6 +242,17 @@ enum : DWORD { ERROR_INVALID_PARAMETER = 87, ERROR_CALL_NOT_IMPLEMENTED = 120, ERROR_NEGATIVE_SEEK = 131, + + // A name that exists and is not a form this loader can start. Version 0.13, + // kal_err_not_program. Measured under Wine (`kal_process_spawn` upon a text + // file with no recognised extension): ERROR_BAD_EXE_FORMAT. The second is + // this loader's documented report for an image built for another processor, + // which is the same condition under a different cause and has no separate + // value in openkal's closed set to be folded into. + ERROR_INVALID_EXE_SIGNATURE = 191, + ERROR_EXE_MARKED_INVALID = 192, + ERROR_BAD_EXE_FORMAT = 193, + ERROR_EXE_MACHINE_TYPE_MISMATCH = 216, ERROR_DISK_FULL = 112, ERROR_INVALID_NAME = 123, ERROR_FILENAME_EXCED_RANGE = 206, @@ -284,6 +314,13 @@ OKW_IMPORT BOOL OKW_API GetConsoleScreenBufferInfo(HANDLE, CONSOLE_SCREEN_BUFFER OKW_IMPORT BOOL OKW_API ReadFile(HANDLE, LPVOID, DWORD, DWORD*, OVERLAPPED*); OKW_IMPORT BOOL OKW_API WriteFile(HANDLE, LPCVOID, DWORD, DWORD*, OVERLAPPED*); OKW_IMPORT BOOL OKW_API FlushFileBuffers(HANDLE); +// For a transfer upon an overlapped socket, version 0.13: an event of its own +// and a synchronous wait for the one operation that used it, so that two +// directions of a connection do not contend for one handle-wide completion +// signal. src/net.cpp and src/datagram.cpp say why the socket is overlapped at +// all. +OKW_IMPORT HANDLE OKW_API CreateEventW(SECURITY_ATTRIBUTES*, BOOL, BOOL, LPCWSTR); +OKW_IMPORT BOOL OKW_API GetOverlappedResult(HANDLE, OVERLAPPED*, DWORD*, BOOL); OKW_IMPORT BOOL OKW_API SetFilePointerEx(HANDLE, LARGE_INTEGER, LARGE_INTEGER*, DWORD); OKW_IMPORT HANDLE OKW_API CreateFileW(LPCWSTR, DWORD, DWORD, SECURITY_ATTRIBUTES*, DWORD, DWORD, HANDLE); @@ -306,10 +343,22 @@ OKW_IMPORT BOOL OKW_API CreateProcessW(LPCWSTR, LPWSTR, SECURITY_ATTRIBUTES*, OKW_IMPORT BOOL OKW_API GetExitCodeProcess(HANDLE, DWORD*); OKW_IMPORT BOOL OKW_API TerminateProcess(HANDLE, UINT); +// The explicit handle-inheritance list, version 0.13. `lpSize' is filled by a +// first call with a null list and consulted by a second that allocates a +// buffer of that size; `UpdateProcThreadAttribute' then attaches the array of +// handles this operation placed, under `PROC_THREAD_ATTRIBUTE_HANDLE_LIST'. +using LPPROC_THREAD_ATTRIBUTE_LIST = void*; +OKW_IMPORT BOOL OKW_API InitializeProcThreadAttributeList( + LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD, unsigned long long*); +OKW_IMPORT BOOL OKW_API UpdateProcThreadAttribute( + LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, unsigned long long, LPVOID, + unsigned long long, LPVOID, unsigned long long*); +OKW_IMPORT void OKW_API DeleteProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST); + // openkal 0.11: the unit a set of started programs forms. A job object ends its // members as one, which is what `kal_process_job_terminate' is. // -// ⚠️ NO `SetInformationJobObject' HERE, AND ITS ABSENCE IS THE DESIGN. The limit +// NO `SetInformationJobObject' HERE, AND ITS ABSENCE IS THE DESIGN. The limit // that ends members when the last handle closes --- JOB_OBJECT_LIMIT_KILL_ON_JOB_ // CLOSE --- is exactly what must NOT be set: `kal_process_job_close' releases and // does not end, because where a unit is a process group closing is releasing a @@ -332,7 +381,7 @@ OKW_IMPORT void OKW_API Sleep(DWORD); OKW_IMPORT BOOL OKW_API SwitchToThread(void); // The address-based wait, which is what openkal.task's suspension primitive -// rests on here. ⚠️ In `API-MS-Win-Core-Synch-l1-2-0`, which is why the link +// rests on here. In `API-MS-Win-Core-Synch-l1-2-0`, which is why the link // line names `-lsynchronization` rather than only `-lkernel32`. OKW_IMPORT BOOL OKW_API WaitOnAddress(volatile void*, void*, unsigned long long, DWORD); // For kal_process_spawn: one start at a time marks handles for inheritance. A @@ -396,7 +445,7 @@ OKW_IMPORT DWORD OKW_API RtlNtStatusToDosError(long); // ── ws2_32: this system's network interface ───────────────────────────────── // -// ⚠️⚠️ NOT DECLARED AS IMPORTS AND NOT LINKED, AND THE REASON IS A COLLISION +// NOT DECLARED AS IMPORTS AND NOT LINKED, AND THE REASON IS A COLLISION // RATHER THAN A PREFERENCE. // // This library's names ARE the BSD names --- `bind', `listen', `accept', @@ -413,13 +462,13 @@ OKW_IMPORT DWORD OKW_API RtlNtStatusToDosError(long); // import library's member defines the thunk AND the `__imp_' pointer together, // so reaching for either brings both. // -// ⭐ THE NAMES ARE THEREFORE REACHED AT RUN TIME, THROUGH THE LIBRARY'S OWN +// THE NAMES ARE THEREFORE REACHED AT RUN TIME, THROUGH THE LIBRARY'S OWN // LOADER. Nothing of ws2_32 enters this program's symbol table, so the C // library above keeps its `bind' and this implementation still reaches the // system's. `ws2_32.dll' is a core component of every installation of this // system, and src/endpoint.h states what happens if it is somehow absent. // -// ⚠️ AND THREE CONSTANTS DIFFER FROM THE OTHER SYSTEMS' WITHOUT ANNOUNCING IT: +// AND THREE CONSTANTS DIFFER FROM THE OTHER SYSTEMS' WITHOUT ANNOUNCING IT: // `AF_INET6' is 23 here, 30 on macOS and 10 on Linux; `SOL_SOCKET' is 0xffff // here and on macOS and 1 on Linux; and this system's `poll' has no bit named // POLLIN --- what it has is POLLRDNORM, and a caller that passed the Linux @@ -438,6 +487,10 @@ enum : int { SOCK_STREAM_ = 1, SOCK_DGRAM_ = 2, IPPROTO_TCP_ = 6, IPPROTO_UDP_ = 17, SD_RECEIVE_ = 0, SD_SEND_ = 1, SD_BOTH_ = 2, + // Version 0.13: every socket this implementation makes is overlapped, so + // that its two directions transfer through their own OVERLAPPED rather than + // contend for the one completion event a synchronous handle has. + WSA_FLAG_OVERLAPPED_ = 0x01, }; // What this system's `poll' names its bits. POLLRDNORM and POLLWRNORM are what @@ -470,8 +523,11 @@ struct ksockaddr_in6 { struct ksockaddr_storage { unsigned char pad[128]; }; +// One buffer, for the overlapped forms of send and receive. Version 0.13. +struct WSABUF_ { DWORD len; char* buf; }; + // The shapes of the calls, so that a pointer obtained at run time is still -// type-checked. ⚠️ THE LAYOUT RULE OF THIS FILE APPLIES HERE TOO: a signature +// type-checked. THE LAYOUT RULE OF THIS FILE APPLIES HERE TOO: a signature // that is wrong does not fail to compile, because nothing checks it against the // system --- it produces a call with the wrong arguments in the wrong places. using pfn_WSAStartup = int (OKW_API*)(WORD, void*); @@ -488,3 +544,10 @@ using pfn_getpeername = int (OKW_API*)(SOCKET, void*, int*); using pfn_sendto = int (OKW_API*)(SOCKET, const char*, int, int, const void*, int); using pfn_recvfrom = int (OKW_API*)(SOCKET, char*, int, int, void*, int*); using pfn_WSAPoll = int (OKW_API*)(WSAPOLLFD_*, ULONG, int); +// Version 0.13: the overlapped forms, so that a datagram socket's send and +// receive transfer through their own OVERLAPPED rather than the one completion +// event a synchronous handle's `sendto'/`recvfrom' would contend upon. +using pfn_WSASendTo = int (OKW_API*)(SOCKET, WSABUF_*, DWORD, DWORD*, DWORD, + const void*, int, OVERLAPPED*, LPVOID); +using pfn_WSARecvFrom = int (OKW_API*)(SOCKET, WSABUF_*, DWORD, DWORD*, DWORD*, + void*, int*, OVERLAPPED*, LPVOID); diff --git a/tests/handle_inheritance.cpp b/tests/handle_inheritance.cpp new file mode 100644 index 0000000..85a6c06 --- /dev/null +++ b/tests/handle_inheritance.cpp @@ -0,0 +1,132 @@ +// A regression test for version 0.13: a spawn inherits only the handles it +// placed. +// +// Before the fix, kal_process_spawn called CreateProcessW with inheritance +// enabled whenever any stream was placed, and bInheritHandles does not +// discriminate: the started program received every inheritable handle of this +// process, not only the ones this operation named. A detached child then kept +// a starter's own standard output open long after the starter had gone, and +// whoever waited for the end of that pipe waited for the wrong program to end. +// +// This test builds the condition directly. It makes one end of a pipe +// inheritable for a reason of its own -- exactly as a caller's own handle may +// legitimately be -- and never places it in a spawn; it starts a long-lived +// child with one legitimate placed stream (the child's own standard input, +// which keeps a shell reading from it alive); and it closes its own copy of +// the unplaced end. If the fix holds, nothing else holds that handle and the +// pipe's other end reports the end of input promptly; if the leak is present, +// the child's own duplicate keeps the pipe open and the bounded read does not +// complete within the bound -- which is what turns a hang into a report. +#include +#include +#include "../src/win32.h" +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool held, const char* what) { + if (held) { std::printf("ok: %s\n", what); return; } + std::printf("FAIL: %s\n", what); + ++failures; +} + +// A volume, found among the preopens by its own name: exactly a letter and a +// colon, which is how src/fs.cpp reports one and how it distinguishes a +// volume from the working directory beside it (whose own name is a full path +// and is therefore longer). openkal names no absolute path; every volume is +// supplied as a directory a name may be resolved relative to. +bool find_a_volume(kal_dir* out) { + const kal_uintptr n = kal_fs_preopen_count(); + for (kal_uintptr i = 0; i < n; ++i) { + char name[16]{}; + kal_uintptr len = 0; + kal_dir d{}; + if (kal_fs_preopen(i, &d, name, sizeof name, &len) != kal_ok) continue; + if (len == 2 && name[1] == ':') { *out = d; return true; } + } + return false; +} + +} // namespace + +int main() { + kal_dir sys{}; + if (!find_a_volume(&sys)) { + std::printf("SKIP: no volume was found among the preopens\n"); + return 0; + } + + // The handle this test makes inheritable and never places. A real caller + // has legitimate reasons to hold an inheritable handle it does not intend + // for THIS spawn -- one made for a different, earlier or later, start. + SECURITY_ATTRIBUTES sa{}; + sa.nLength = sizeof sa; + sa.bInheritHandle = TRUE; + HANDLE leak_read = nullptr, leak_write = nullptr; + if (!CreatePipe(&leak_read, &leak_write, &sa, 0)) { + std::printf("SKIP: the pipe for the leak this test looks for could not be made\n"); + return 0; + } + + // The one stream this spawn legitimately places: the child's standard + // input, which keeps a shell reading from it alive until told to stop. + kal_stream mine{}, child_stdin{}; + if (kal_process_channel(&mine, &child_stdin) != kal_ok) { + std::printf("SKIP: a channel for the child's standard input could not be made\n"); + CloseHandle(leak_read); + CloseHandle(leak_write); + return 0; + } + + const char* path = "Windows/System32/cmd.exe"; + const kal_uintptr path_len = std::strlen(path); + const kal_spawn how{ sys, sys, nullptr, nullptr, 0, 0 }; + const kal_spawn_streams streams{ child_stdin, kal_stream{0}, kal_stream{0} }; + kal_process child{}; + const int spawned = kal_process_spawn(&how, path, path_len, &path, &path_len, 1, + nullptr, nullptr, 0, &streams, &child); + if (spawned != kal_ok) { + std::printf("SKIP: cmd.exe could not be started (kal error %d)\n", spawned); + kal_process_channel_close(mine); + kal_process_channel_close(child_stdin); + CloseHandle(leak_read); + CloseHandle(leak_write); + return 0; + } + // The child's own copy of its standard input. This end belongs to the + // child now; this process keeps `mine', which is what lets it end the + // shell later. + kal_process_channel_close(child_stdin); + + // The handle this test made inheritable and never placed, closed here on + // this process's side only. Whether anything else still holds it is + // exactly what the read below observes. + CloseHandle(leak_write); + + char buf[8]; + const kal_stream leak_stream{ reinterpret_cast(leak_read) }; + const kal_intptr r = kal_timeout_read(leak_stream, buf, sizeof buf, + 5000ull * 1000ull * 1000ull); + check(r == 0, + "a handle made inheritable and not placed in the spawn is not inherited by it"); + + // End the shell and wait for it, so the process does not outlive this + // test whether the observation above held or not. + const char exit_cmd[] = "exit\r\n"; + kal_stream_write(mine, exit_cmd, sizeof exit_cmd - 1); + kal_process_channel_close(mine); + int status = 0, terminated = 0; + if (kal_timeout_wait_process(child, 5000ull * 1000ull * 1000ull, &status, &terminated) != kal_ok) + kal_process_terminate(child); + kal_process_close(child); + CloseHandle(leak_read); + + std::printf("openkal-windows: a spawn inherits only the handles it placed\n"); + return failures == 0 ? 0 : 1; +} From 09e80ad772bbebd36099a431b02ed40c0ed14e12 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 17 Sep 2026 16:48:25 +0800 Subject: [PATCH 2/4] fix: an overlapped datagram truncation is ERROR_MORE_DATA, not WSAEMSGSIZE Measured on windows-2022 (this PR's own CI): a message too large for the buffer, received through the overlapped WSARecvFrom this version introduced, completes with STATUS_BUFFER_OVERFLOW; GetOverlappedResult reports that through the generic Win32 channel as ERROR_MORE_DATA, not as the Winsock-specific WSAEMSGSIZE the immediate-failure path gives and the rest of src/datagram.cpp already checks for. The two are normalised to one where the overlapped result is read, so the truncation this interface requires to be reported as a success (the bytes that fit, not a failure) is recognised regardless of which path the completion took. Wine did not reproduce the distinction; both toolchain rows on windows-2022 did. --- src/datagram.cpp | 18 +++++++++++++++++- src/win32.h | 7 +++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/datagram.cpp b/src/datagram.cpp index f7b34d2..5d31cd6 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -32,6 +32,18 @@ constexpr kal_uintptr kMaxOne = 0x7fffffffu; // One overlapped operation upon the socket: an event of its own, issued and // waited for synchronously. `err' carries the WSA error when this reports // failure; a caller that must translate it uses okw::translate_wsa. +// +// A DATAGRAM TOO LARGE FOR THE BUFFER IS REPORTED TWO WAYS, AND BOTH ARE +// NORMALISED TO ONE HERE. `WSARecvFrom' that fails immediately is read through +// `WSAGetLastError', which gives `WSAEMSGSIZE' --- the Winsock-specific value +// the rest of this file already expects. One that goes pending completes with +// `STATUS_BUFFER_OVERFLOW', and `GetOverlappedResult' reports that through the +// generic channel as `ERROR_MORE_DATA', a different number for the same +// condition. Measured on windows-2022, where the pending path is the one this +// operation actually takes: the immediate path was never reached in that +// measurement, and reporting `ERROR_MORE_DATA' unnormalised left the +// truncation this interface is required to report as a success reported as an +// unrecognised failure instead. bool overlapped_once(SOCKET s, bool send, WSABUF_& wsabuf, DWORD flags, void* addr, int* addrlen, DWORD* moved, int* err) { auto* n = okw::net_or_null(); @@ -50,7 +62,11 @@ bool overlapped_once(SOCKET s, bool send, WSABUF_& wsabuf, DWORD flags, const int e = n->last_error(); if (e == static_cast(ERROR_IO_PENDING)) { ok = GetOverlappedResult(reinterpret_cast(s), &ov, moved, TRUE) != 0; - if (!ok) *err = static_cast(GetLastError()); + if (!ok) { + const DWORD ge = GetLastError(); + *err = ge == ERROR_MORE_DATA ? static_cast(okw::WSAEMSGSIZE) + : static_cast(ge); + } } else { *err = e; } diff --git a/src/win32.h b/src/win32.h index 3fb0f2d..45ae00c 100644 --- a/src/win32.h +++ b/src/win32.h @@ -260,6 +260,13 @@ enum : DWORD { ERROR_NO_DATA = 232, ERROR_PIPE_NOT_CONNECTED = 233, ERROR_TIMEOUT = 1460, + // The generic report of what WSAEMSGSIZE is upon a synchronous completion. + // A datagram received through an overlapped WSARecvFrom that does not fit + // the buffer completes with STATUS_BUFFER_OVERFLOW, and GetOverlappedResult + // reports that as this value rather than as the Winsock-specific one; only + // WSAGetLastError, reached on the path that fails immediately, gives the + // latter. Version 0.13; measured on windows-2022. + ERROR_MORE_DATA = 234, ERROR_FILE_NOT_FOUND = 2, ERROR_PATH_NOT_FOUND = 3, ERROR_NO_MORE_FILES = 18, From 2fc10cb9d27ab95c8460324eb371c4ec05e8979a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 17 Sep 2026 17:15:32 +0800 Subject: [PATCH 3/4] ci: This package's own tests takes the matrix's --target, as the conformance step beside it already does The gcc/gnu-CRT row's mcpp test, run with no --target, did not pick up [target.'cfg(all(windows, not(env = "msvc")))'.build].ldflags -- the import libraries this package's own objects need (ntdll, synchronization, bcrypt) -- and every test failed to link: undefined references to __imp_NtCreateFile, __imp_WaitOnAddress, __imp_BCryptGenRandom, and the rest, all from openkal-windows's own objects the test links against. Passing --target x86_64-windows-gnu, matching the matrix and the step beside it, resolves it: a local cross build with the same flag links and runs handle_inheritance cleanly. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e193235..b82a140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -286,7 +286,9 @@ jobs: # suite, which has no vocabulary for a handle outside openkal's own. - name: This package's own tests run: | - mcpp test 2>&1 | tee tests.log + extra='' + [ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}' + mcpp test $extra 2>&1 | tee tests.log missing=0 for f in tests/*.cpp; do name="$(basename "$f" .cpp)" From 1160fb9b995617d9d9292ab315aa48f62234a660 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 17 Sep 2026 17:48:05 +0800 Subject: [PATCH 4/4] README: a connection's stream crossing a spawn is overlapped Recorded after review. 0.7 made sockets synchronous so that a started program could treat one as a pipe; 0.8 makes them overlapped for the independence of the two directions. A started program that issues synchronous transfers on an inherited socket is outside what the system defines, and a caller relays the connection through a channel instead. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 948a9e0..f097e6e 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ The four that openkal 0.8 added and this implementation now provides: | | on this system | | --- | --- | -| `openkal.net` | Winsock, started once at the first socket and never stopped. Every socket is made with `WSA_FLAG_OVERLAPPED`, and version 0.13 requires it: a synchronous socket shares one completion event between its two directions, so a read waiting in one thread held back a write from another on the same connection. The transfer operations issue `ReadFile`/`WriteFile` with an `OVERLAPPED` of their own and wait for it, so the two directions no longer contend for one event | +| `openkal.net` | Winsock, started once at the first socket and never stopped. Every socket is made with `WSA_FLAG_OVERLAPPED`, and version 0.13 requires it: a synchronous socket shares one completion event between its two directions, so a read waiting in one thread held back a write from another on the same connection. The transfer operations issue `ReadFile`/`WriteFile` with an `OVERLAPPED` of their own and wait for it, so the two directions no longer contend for one event. A connection's stream placed in `kal_spawn_streams` reaches the started program as an overlapped handle: a program on this implementation transfers upon it correctly, and a program that issues synchronous `ReadFile`/`WriteFile` upon an inherited handle without an `OVERLAPPED` is outside what the system defines for a socket. A caller that starts such a program relays the connection through a channel (`kal_process_channel`) instead | | `openkal.datagram` | the same calls with `SOCK_DGRAM`, `WSASendTo`/`WSARecvFrom` with their own `OVERLAPPED` for the same reason. This system reports a truncated message as a **failure** where the other two truncate silently; the bytes that fit are delivered either way, and the interface says the excess is lost | | `openkal.timeout` | `WSAPoll`, which answers for sockets and for nothing else. A bounded read of a stream that is not a socket reports `kal_err_not_supported` — which the interface's own header anticipates in terms. `kal_timeout_wait_process` is the one operation of the interface this system provides **directly**, because a bounded wait upon an object is the primitive here | | `openkal.exec` | `VirtualAlloc` writable, `VirtualProtect` executable, `FlushInstructionCache`. The third call is not optional and the other two systems' implementations do not need to make it explicit |