diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0e6ebf..b82a140 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,39 @@ 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: | + 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)" + 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 +321,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 +335,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 +344,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 +381,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 +391,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 +410,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 +423,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 +437,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 +467,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..f097e6e 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. 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 | +## 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..5d31cd6 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,52 @@ 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. +// +// 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(); + 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) { + const DWORD ge = GetLastError(); + *err = ge == ERROR_MORE_DATA ? static_cast(okw::WSAEMSGSIZE) + : static_cast(ge); + } + } else { + *err = e; + } + } + CloseHandle(ev); + return ok; +} + } // namespace extern "C" { @@ -42,7 +94,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 +136,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 +154,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 +164,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 +184,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 +195,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 +208,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..45ae00c 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, @@ -230,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, @@ -284,6 +321,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 +350,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 +388,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 +452,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 +469,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 +494,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 +530,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 +551,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; +}