diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3076fa5..6a1dbbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,7 +210,8 @@ jobs: headers = sorted(glob.glob("src/*.h")) for h in headers: text = open(h, encoding="utf-8").read() - declared |= set(re.findall(r'OKW_IMPORT\s+\w+\s+OKW_API\s+(\w+)\s*\(', text)) + # A return type of several words (`unsigned long long') too. + declared |= set(re.findall(r'OKW_IMPORT\s+[\w\s\*]+?\s+OKW_API\s+(\w+)\s*\(', text)) declared |= set(re.findall( r'__declspec\(dllimport\)[\w\s\*]*?(\w+)\s*\(', text)) exported = set() @@ -258,11 +259,11 @@ jobs: # `kal_space_start`, which is clause 6.1's report arriving where a # report was not wanted. # - # So the six this implementation does provide are named, and the - # seventh is absent from the list for the same reason it is absent + # So the ones this implementation does provide are named, and + # `space` is absent from the list for the same reason it is absent # from the objects. bash .spec/tools/run-conformance.sh openkal-windows . \ - full,exec,random,terminal,net,datagram,timeout,map $extra + full,exec,random,terminal,net,datagram,timeout,map,region,prof,debug $extra # run-conformance.sh above has already pointed the manifest at the # specification's working tree and restored it through a trap; this @@ -447,7 +448,7 @@ jobs: WINEDEBUG: '-all' run: | bash .spec/tools/run-conformance.sh openkal-windows . \ - full,exec,random,terminal,net,datagram,timeout,map \ + full,exec,random,terminal,net,datagram,timeout,map,region,prof,debug \ --target x86_64-windows-gnu - name: The exported surface is complete and contains nothing else @@ -458,7 +459,7 @@ jobs: # interface: a name it fails to export is a failure, not an interface # it declines to provide. rm -rf target && mcpp build --target x86_64-windows-gnu - objs="$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' | tr '\n' ' ')" + objs="$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -path '*/.build-mcpp/*' | tr '\n' ' ')" test -n "$objs" || { echo "no objects were found" >&2; exit 1; } bash .spec/tools/check-surface.sh --complete .spec/SURFACE.txt $objs @@ -489,7 +490,7 @@ jobs: for profile in dev release; do echo "--- profile $profile" rm -rf target && mcpp build --features standalone --target x86_64-windows-gnu --profile "$profile" - objs="$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o')" + objs="$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -path '*/.build-mcpp/*')" test -n "$objs" || { echo "no objects were found; the check would pass vacuously" >&2; exit 1; } # The permitted set, and why each entry is in it. @@ -526,7 +527,7 @@ jobs: run: | printf 'extern "C" int puts(const char*);\nextern "C" void okw_probe(void) { puts("x"); }\n' > src/probe.cpp rm -rf target && mcpp build --features standalone --target x86_64-windows-gnu - objs="$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o')" + objs="$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -path '*/.build-mcpp/*')" if ! nm --undefined-only $objs | awk '{print $2}' | grep -qx puts; then echo "the probe did not produce the reference it was written to produce" >&2 rm -f src/probe.cpp; exit 1 diff --git a/README.md b/README.md index 8ee31eb..1bc7865 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.22.0" +openkal = "0.23.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.18.0" +openkal-windows = "0.19.0" ``` Its purpose is as much to test the specification as to be used. openkal was @@ -142,6 +142,61 @@ observes is the part a caller depends on rather than the choice between the two: the region contains the stack of the thread that asked. It is measured on this system's own runner and again under Wine on a Linux one. +## Version 0.19 (openkal 0.23) + +**A region at an address.** `kal_exec_alloc_at` reserves at the address, and +nothing where anything is there or the address is not a multiple of the +sixty-four kibibytes of reservation. `KAL_EXEC_PROP_PLACE` is claimed. + +**A part of a region is a file's** (`KAL_EXEC_PROP_FILE`, where the system has +placeholders: Windows 10 1803). A region is a placeholder replaced by a view of a +section of the paging file of the region's size. A part made a file's is the +view unmapped back to a placeholder, split, the part replaced by a view of the +file and the rest by views of the same section at the same offsets, which hold +what they held; the region is a short table of those views. Views are placed in +sixty-four kibibytes, so a part not at that granularity is `kal_err_not_supported` +and the region is unchanged. `KAL_EXEC_MAP_PREFETCH` is `PrefetchVirtualMemory`. +`kal_exec_seal_part` makes a part read-only; `KAL_EXEC_PROP_SEAL_FOREVER` is not +claimed, because the holder of a view can always change its protection. + +**Grants.** `KAL_PROCESS_PROP_GRANT_DIR` is claimed. A grant is a duplicate of the +caller's handle, inheritable for the one start and named in its explicit list; +an inherited handle keeps its value, and the started program learns the values, +the names and what each carries from `KAL_PREOPENS` and `KAL_GRANT_RIGHTS`, read +only where the number in them is the program that started it. A grant is made +uninheritable as it is taken over. `KAL_SPAWN_CONFINE` is refused: this system +holds a program to directories only through the files' security descriptors, +which a start does not change. + +**Attenuation and records** (`kal_fs_dir_attenuate`): the table of the other two +implementations, indexed by a handle's slot. A listing answers that a name is +absent only where no other spelling could reach an entry: a name of ASCII with +no `~` (a short name the listing does not report), in a directory whose entries +are ASCII. A record is appended through a handle of its own opened with +`FILE_APPEND_DATA`, and ended by `kal_exit` or the runtime's terminators. + +**One request where the system has one.** A listing reads +`FileIdFullDirectoryInformation`, whose records carry each entry's size, times, +attributes and identity (`kal_fs_list_next_info`; `KAL_FS_PROP_LIST_INFO` where +the volume states `FILE_SUPPORTS_OPEN_BY_FILE_ID`). `kal_fs_info` asks +`NtQueryInformationByName` for the node's record --- with the volume's serial +number from 24H2 (`FileStatBasicInformation`), and otherwise +(`FileStatInformation`, 1709) with the base directory's for a name of one +component --- and opens the node where the record cannot answer: a link to be +resolved, an identity across components, a volume that refuses the record. + +**Observation.** A fault no handler takes is reported, by the filter this system +calls before it ends the program, with each frame's file, offset and symbol: the +file a part of a region maps (an ELF image) or the module (a PE image, from its +COFF symbol table). Samples are taken by a context of the program's own that +suspends each other context that used the processor since the last tick. +Touches are a part's execution taken away and given back, page by page, by a +first look at the access violations of the kind that executes. +`kal_process_info` reports page faults as one count, `faults_minor`. +`kal_process_wait` reports a program this system ended for a fault as +terminated. A preference for economy lowers the started program's memory +priority; `kal_memory_info` is `GlobalMemoryStatusEx` within the job's limits. + ## Version 0.18 (openkal 0.22) **The null device at a position.** `KAL_SPAWN_NULL_IN`, `KAL_SPAWN_NULL_OUT` and diff --git a/mcpp.toml b/mcpp.toml index b80f70c..b5138e2 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.18.0" +version = "0.19.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" @@ -29,7 +29,7 @@ repo = "https://github.com/mcpplibs/openkal-windows" # the first of the three times clause 6.2 tabulates, and the earliest at which # the question can be answered. # -# FOURTEEN AND NOT FIFTEEN. `openkal.space` is absent: this environment has no +# EVERY ONE BUT ONE. `openkal.space` is absent: this environment has no # operation upon an address space it can supply, and clause 3 says an # implementation provides an interface in whole or not at all. A consumer that # needs it is refused here and builds on openkal-linux, which is the whole @@ -61,10 +61,12 @@ provides-interfaces = [ "openkal.timeout", "openkal.map", "openkal.region", + "openkal.prof", + "openkal.debug", ] [dependencies] -openkal = "0.22.0" +openkal = "0.23.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/port/kernel32.def b/port/kernel32.def index 2bf80e6..86c4a46 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -5,6 +5,7 @@ LIBRARY KERNEL32.dll EXPORTS AcquireSRWLockExclusive +AddVectoredExceptionHandler AssignProcessToJobObject CancelIoEx CloseHandle @@ -17,8 +18,11 @@ CreatePipe CreateProcessW CreateSymbolicLinkW CreateThread +CreateToolhelp32Snapshot +CreateWaitableTimerExW DeleteProcThreadAttributeList DeviceIoControl +DuplicateHandle ExitProcess FlushFileBuffers FlushInstructionCache @@ -53,22 +57,31 @@ GetQueuedCompletionStatus GetStdHandle GetSystemInfo GetSystemTimePreciseAsFileTime +GetThreadContext +GetThreadTimes +GlobalMemoryStatusEx HeapAlloc HeapFree InitializeProcThreadAttributeList IsProcessInJob +K32GetMappedFileNameW K32GetProcessMemoryInfo LoadLibraryW LocalFree MapViewOfFile MultiByteToWideChar OpenProcess +OpenThread PeekNamedPipe QueryInformationJobObject QueryPerformanceCounter QueryPerformanceFrequency +QueryThreadCycleTime ReadFile +ReadProcessMemory ReleaseSRWLockExclusive +ReleaseSRWLockShared +RemoveVectoredExceptionHandler ResumeThread SetConsoleCtrlHandler SetConsoleMode @@ -76,15 +89,22 @@ SetFilePointerEx SetHandleInformation SetInformationJobObject SetThreadPriority +SetUnhandledExceptionFilter +SetWaitableTimer Sleep +SuspendThread SwitchToThread TerminateJobObject TerminateProcess +Thread32First +Thread32Next +TryAcquireSRWLockShared UnmapViewOfFile UpdateProcThreadAttribute VirtualAlloc VirtualFree VirtualProtect +VirtualQuery WaitForSingleObject WideCharToMultiByte WriteConsoleW diff --git a/port/ntdll.def b/port/ntdll.def index 09750ec..64d66b3 100644 --- a/port/ntdll.def +++ b/port/ntdll.def @@ -11,6 +11,7 @@ NtFlushBuffersFile NtLockFile NtQueryDirectoryFile NtQueryInformationFile +NtQueryInformationProcess NtQueryVolumeInformationFile NtReadFile NtSetInformationFile diff --git a/src/abort.cpp b/src/abort.cpp index bbdef0b..2fed28b 100644 --- a/src/abort.cpp +++ b/src/abort.cpp @@ -1,4 +1,5 @@ #include "win.h" +#include "rights.h" #include extern "C" { @@ -34,6 +35,9 @@ extern "C" { // notifies every loaded module before the process ends, and a module's // notification is code the caller did not ask to run. [[noreturn]] void kal_exit(int code) { + // What the program's attenuated handles recorded is written, and ended + // (0.23, fs.h). + okw::rights::end_of_program(); TerminateProcess(GetCurrentProcess(), static_cast(code)); for (;;) { } } diff --git a/src/env.cpp b/src/env.cpp index 56dcdd2..465779c 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -208,4 +208,41 @@ kal_intptr kal_env_system(char* out, kal_uintptr cap) { return give("windows", 7, out, cap); } +// THE RESOURCE PREFERENCE (0.23): what the starter conveyed in KAL_RESOURCES +// (process.cpp), read once; balanced where nothing was conveyed. +int kal_env_resources(void) { + static volatile long long answer = -1; + long long a = okw::atomic::load(&answer); + if (a < 0) { + char v[16]; + const kal_intptr n = kal_env_var("KAL_RESOURCES", 13, v, sizeof v); + a = KAL_RESOURCES_BALANCED; + if (n == 11 && same(v, 11, "performance", 11)) a = KAL_RESOURCES_PERFORMANCE; + else if (n == 7 && same(v, 7, "economy", 7)) a = KAL_RESOURCES_ECONOMY; + okw::atomic::store(&answer, a); + } + return static_cast(a); +} + +} + +namespace okw { +// ECONOMY ON THIS SYSTEM: the program's pages are the first taken back when +// memory is short (ProcessMemoryPriority, MEMORY_PRIORITY_LOW). +void lower_memory_priority(void* process) { + const auto set = reinterpret_cast( + system_function(L"kernel32.dll", "SetProcessInformation")); + if (!set) return; + unsigned long priority = 2; // MEMORY_PRIORITY_LOW + set(process, 0 /* ProcessMemoryPriority */, &priority, sizeof priority); } +} // namespace okw + +namespace { +// A program started for economy by a starter that did not set it (one that is +// not of this implementation, or a person) sets it on itself. +OKW_START_FN(economy_at_start) { + if (kal_env_resources() == KAL_RESOURCES_ECONOMY) okw::lower_memory_priority(GetCurrentProcess()); +} +OKW_AT_START(economy_at_start); +} // namespace diff --git a/src/exec.cpp b/src/exec.cpp index b60c595..c507b59 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -1,5 +1,8 @@ #include "win.h" +#include "handle.h" #include +#include +#include // openkal.exec on this system. // @@ -18,35 +21,299 @@ // architecture this package is built for it is inexpensive, and on the other // architecture this system runs on it is the difference between executing what // was written and executing what was there before. +// +// A PART OF A REGION THAT IS A FILE'S (0.21, provided from 0.19.0). This system +// maps a view of a file only where nothing is mapped, or where a PLACEHOLDER of +// exactly the view's extent is (Windows 10 1803): a reservation that holds no +// memory yet, which a view or an allocation replaces and which a view gives +// back when it is unmapped. So a region is made of placeholders from the start: +// one, reserved for the whole region and replaced by a view of a section of the +// region's own size backed by the paging file (`made'). Its bytes are the +// section's, not the view's, and that is what lets a part become a file's later +// without losing the rest: the view is unmapped back to a placeholder, the +// placeholder split at the part, the part replaced by a view of the file and +// the rest by views of the same section at the same offsets, which hold what +// they held. A region is then a short table of such views (`region', below); +// clause 7.1 admits it because this system's view is the only shape there is, +// and the table is one entry per view the caller asked for and nothing else. +// +// Views are placed in this system's sixty-four kibibytes, so a part whose +// offset or size is not a multiple of them is refused with +// kal_err_not_supported and the region is unchanged; a loader copies such a +// part, and lays an image out in those blocks (clause 12). +// +// WHERE THE SYSTEM HAS NO PLACEHOLDERS a region is an allocation of its own, as +// it was before 0.19.0, and KAL_EXEC_PROP_FILE is not claimed. namespace { -// The reservation granularity. `VirtualAlloc' rounds a size up to a page and an -// address down to the allocation granularity, so a caller's size is rounded -// here only so that the reservation and the release agree about the number --- -// which they must, because the release takes the size the reservation was given. constexpr unsigned long long kPage = 4096; +constexpr unsigned long long kView = 65536; unsigned long long round_up(unsigned long long n, unsigned long long to) { return (n + to - 1) & ~(to - 1); } +// --- the placeholders ------------------------------------------------------------ +struct calls { + pfn_VirtualAlloc2_ alloc2 = nullptr; + pfn_MapViewOfFile3_ map3 = nullptr; + pfn_UnmapViewOfFile2_ unmap2 = nullptr; + pfn_PrefetchVirtualMemory_ prefetch = nullptr; + bool usable = false; +}; + +// A VIEW OF THE REGION'S SECTION is mapped with every access a region's part +// will be given and made writable at once: a view's protection may later be +// changed only to one its mapping allowed, and a part of a region is published +// executable. Nothing is in the view between the two calls, which are this +// implementation's own; the caller receives it writable and not executable. +bool section_view(const calls& c, HANDLE section, char* at, unsigned long long from, unsigned long long len) { + if (c.map3(section, GetCurrentProcess(), at, from, len, MEM_REPLACE_PLACEHOLDER_, PAGE_EXECUTE_READWRITE, + nullptr, 0) != at) + return false; + DWORD before = 0; + if (VirtualProtect(at, len, PAGE_READWRITE, &before)) return true; + UnmapViewOfFile(at); + return false; +} + +// A placeholder reserved, split, a view of a section placed in one half and +// made executable, and the view given back to a placeholder: the whole of what +// a region uses, tried once, so that a system that names these calls and does +// not perform them --- an emulator of this system --- is not claimed to. +bool works(const calls& c) { + const HANDLE self = GetCurrentProcess(); + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, + static_cast(2 * kView), nullptr); + if (!section) return false; + auto* p = static_cast(c.alloc2(self, nullptr, 2 * kView, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER_, + PAGE_NOACCESS, nullptr, 0)); + bool ok = p != nullptr && VirtualFree(p, kView, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER_); + ok = ok && section_view(c, section, p, 0, kView); + DWORD before = 0; + if (ok) { p[0] = 1; ok = VirtualProtect(p, kPage, PAGE_EXECUTE_READ, &before) && p[0] == 1; } + ok = ok && c.unmap2(self, p, MEM_PRESERVE_PLACEHOLDER_); + if (p) { + UnmapViewOfFile(p); + VirtualFree(p, 0, MEM_RELEASE); + VirtualFree(p + kView, 0, MEM_RELEASE); + } + CloseHandle(section); + return ok; +} + +SRWLOCK_ g_calls_lock{}; +calls g_calls; +volatile long long g_calls_known = 0; + +const calls& placeholders() { + if (okw::atomic::load(&g_calls_known)) return g_calls; + AcquireSRWLockExclusive(&g_calls_lock); + if (!okw::atomic::load(&g_calls_known)) { + static const wchar_t kernelbase[] = L"kernelbase.dll"; + g_calls.alloc2 = reinterpret_cast(okw::system_function(kernelbase, "VirtualAlloc2")); + g_calls.map3 = reinterpret_cast(okw::system_function(kernelbase, "MapViewOfFile3")); + g_calls.unmap2 = reinterpret_cast(okw::system_function(kernelbase, "UnmapViewOfFile2")); + g_calls.prefetch = reinterpret_cast( + okw::system_function(kernelbase, "PrefetchVirtualMemory")); + g_calls.usable = g_calls.alloc2 && g_calls.map3 && g_calls.unmap2 && works(g_calls); + okw::atomic::store(&g_calls_known, 1); + } + ReleaseSRWLockExclusive(&g_calls_lock); + return g_calls; +} + +// --- the regions made of views --------------------------------------------------- +struct piece { + okw_uptr off, len; + bool file; // a view of a file; otherwise of the region's section + kal_u64 at; // the file's offset, for a file's + bool written; // a file's view written privately (KAL_EXEC_MAP_WRITE) +}; + +struct region { + char* base; + okw_uptr size; + HANDLE section; + piece* pieces; + unsigned count, cap; + region* next; +}; + +region* g_regions = nullptr; +SRWLOCK_ g_regions_lock{}; + +region* find(void* base) { + for (region* r = g_regions; r; r = r->next) if (r->base == base) return r; + return nullptr; +} + +bool grow(region& r) { + const unsigned ncap = r.cap ? r.cap * 2 : 4; + auto* np = static_cast(kal_alloc(ncap * sizeof(piece), alignof(piece))); + if (!np) return false; + for (unsigned i = 0; i < r.count; ++i) np[i] = r.pieces[i]; + if (r.pieces) kal_free(r.pieces, r.cap * sizeof(piece), alignof(piece)); + r.pieces = np; + r.cap = ncap; + return true; +} + +// A region at `at', or anywhere for null: a placeholder of the whole replaced by +// a view of a section of the paging file, whose bytes are zero. +void* made(const calls& c, void* at, kal_uintptr size) { + const unsigned long long bytes = round_up(size, kView); + const HANDLE self = GetCurrentProcess(); + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, + static_cast(bytes >> 32), static_cast(bytes), nullptr); + if (!section) return nullptr; + auto* r = static_cast(kal_alloc(sizeof(region), alignof(region))); + auto* p = static_cast(c.alloc2(self, at, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER_, + PAGE_NOACCESS, nullptr, 0)); + const bool placed = p != nullptr && (at == nullptr || p == at); + if (!r || !placed || !section_view(c, section, p, 0, bytes)) { + if (p) VirtualFree(p, 0, MEM_RELEASE); + if (r) kal_free(r, sizeof(region), alignof(region)); + CloseHandle(section); + return nullptr; + } + *r = region{ p, static_cast(bytes), section, nullptr, 0, 0, nullptr }; + if (!grow(*r)) { + UnmapViewOfFile(p); + kal_free(r, sizeof(region), alignof(region)); + CloseHandle(section); + return nullptr; + } + r->pieces[r->count++] = piece{ 0, static_cast(bytes), false, 0, false }; + AcquireSRWLockExclusive(&g_regions_lock); + r->next = g_regions; + g_regions = r; + ReleaseSRWLockExclusive(&g_regions_lock); + return p; +} + +// The protection of a range of a region: each view of the section in it, as a +// call of this system's own reaches no further than one view. A view of a file +// keeps what it was given (exec.h: it is neither published nor written), unless +// it was given to be written, which is protected as the section's views are. +int protect(void* base, okw_uptr off, okw_uptr size, DWORD how) { + DWORD before = 0; + AcquireSRWLockExclusive(&g_regions_lock); + const region* r = find(base); + if (!r) { + ReleaseSRWLockExclusive(&g_regions_lock); + return VirtualProtect(static_cast(base) + off, size, how, &before) + ? kal_ok : okw::translate_win32(GetLastError()); + } + int e = kal_ok; + for (unsigned i = 0; i < r->count && e == kal_ok; ++i) { + const piece& p = r->pieces[i]; + const okw_uptr lo = p.off > off ? p.off : off; + const okw_uptr hi = p.off + p.len < off + size ? p.off + p.len : off + size; + if (lo >= hi || (p.file && !p.written)) continue; + if (!VirtualProtect(r->base + lo, hi - lo, how, &before)) e = okw::translate_win32(GetLastError()); + } + ReleaseSRWLockExclusive(&g_regions_lock); + return e; +} + +// The protections of a range, as runs, so that a view put back over them has +// them again. +struct runs { + okw_uptr off[32], len[32]; + DWORD prot[32]; + unsigned n = 0; + bool take(const char* base, okw_uptr from, okw_uptr to) { + for (okw_uptr at = from; at < to;) { + MEMORY_BASIC_INFORMATION_ m{}; + if (VirtualQuery(base + at, &m, sizeof m) == 0 || n == 32) return false; + okw_uptr end = static_cast(static_cast(m.BaseAddress) - base) + m.RegionSize; + if (end > to) end = to; + off[n] = at; len[n] = end - at; prot[n] = m.Protect; ++n; + at = end; + } + return true; + } + void give(char* base) const { + DWORD before = 0; + for (unsigned i = 0; i < n; ++i) + if (prot[i] != PAGE_READWRITE) VirtualProtect(base + off[i], len[i], prot[i], &before); + } +}; + +// A handle for the file of its own, with the access a view of it needs. +void* reopen(void* file, unsigned long access) { + okw::unicode_string empty{ 0, 0, nullptr }; + okw::object_attributes attrs{}; + attrs.length = sizeof attrs; + attrs.root_directory = file; + attrs.object_name = ∅ + okw::io_status_block status{}; + void* h = nullptr; + const long rc = okw::NtCreateFile(&h, access | SYNCHRONIZE, &attrs, &status, nullptr, FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, okw::file_open, + okw::file_non_directory_file | okw::file_synchronous_io_nonalert, + nullptr, 0); + return okw::ok(rc) ? h : nullptr; +} + } // namespace +namespace okw { +// The file and its offset where `address' lies in a part of a region that is a +// file's, for the fault report (observe.cpp): false elsewhere, and where the +// table is held by another context at the moment. +bool exec_file_part(okw_uptr address, okw_uptr* view, kal_u64* at) { + if (!TryAcquireSRWLockShared(&g_regions_lock)) return false; + bool found = false; + for (const region* r = g_regions; r && !found; r = r->next) { + const okw_uptr b = reinterpret_cast(r->base); + if (address < b || address >= b + r->size) continue; + for (unsigned i = 0; i < r->count; ++i) { + const piece& p = r->pieces[i]; + if (!p.file || address < b + p.off || address >= b + p.off + p.len) continue; + *view = b + p.off; + *at = p.at; + found = true; + break; + } + } + ReleaseSRWLockShared(&g_regions_lock); + return found; +} +} // namespace okw + extern "C" { void* kal_exec_alloc(kal_uintptr size) { if (size == 0) return nullptr; + const calls& c = placeholders(); + if (c.usable) return made(c, nullptr, size); + return VirtualAlloc(nullptr, round_up(size, kPage), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +} + +// AT AN ADDRESS (0.23): this system's reservation takes an address and fails, +// reserving nothing, where anything is there or the address is not a multiple +// of its sixty-four kibibytes of reservation. +void* kal_exec_alloc_at(void* at, kal_uintptr size) { + if (size == 0 || at == nullptr) return nullptr; + SYSTEM_INFO si{}; + GetSystemInfo(&si); + const unsigned long long quantum = si.dwAllocationGranularity ? si.dwAllocationGranularity : 65536; + if (reinterpret_cast(at) % quantum != 0) return nullptr; + const calls& c = placeholders(); + if (c.usable) return made(c, at, size); const unsigned long long bytes = round_up(size, kPage); - return VirtualAlloc(nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + void* p = VirtualAlloc(at, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (p != nullptr && p != at) { VirtualFree(p, 0, MEM_RELEASE); return nullptr; } + return p; } int kal_exec_publish(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return kal_err_invalid; const unsigned long long bytes = round_up(size, kPage); - DWORD previous = 0; - if (!VirtualProtect(p, bytes, PAGE_EXECUTE_READ, &previous)) - return okw::translate_win32(GetLastError()); + if (const int e = protect(p, 0, bytes, PAGE_EXECUTE_READ); e != kal_ok) return e; if (!FlushInstructionCache(GetCurrentProcess(), p, bytes)) return okw::translate_win32(GetLastError()); return kal_ok; @@ -54,12 +321,24 @@ 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 - // 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 - // invalid-parameter report and leak the reservation. - VirtualFree(p, 0, MEM_RELEASE); + AcquireSRWLockExclusive(&g_regions_lock); + region* r = nullptr; + for (region** at = &g_regions; *at; at = &(*at)->next) + if ((*at)->base == p) { r = *at; *at = r->next; break; } + ReleaseSRWLockExclusive(&g_regions_lock); + if (r == nullptr) { + // 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. + VirtualFree(p, 0, MEM_RELEASE); + return; + } + // Each view, which releases its addresses with it, and the section. + for (unsigned i = 0; i < r->count; ++i) UnmapViewOfFile(r->base + r->pieces[i].off); + CloseHandle(r->section); + kal_free(r->pieces, r->cap * sizeof(piece), alignof(piece)); + kal_free(r, sizeof(region), alignof(region)); } // A published region may be reserved for writing again: this system's @@ -85,29 +364,137 @@ int kal_exec_publish_part(void* region, kal_uintptr offset, kal_uintptr size) { if (region == nullptr || size == 0 || (offset & (g - 1)) != 0 || (size & (g - 1)) != 0 || (reinterpret_cast(region) & (g - 1)) != 0) return kal_err_invalid; - void* at = static_cast(region) + offset; - DWORD previous = 0; - if (!VirtualProtect(at, size, PAGE_EXECUTE_READ, &previous)) - return okw::translate_win32(GetLastError()); - if (!FlushInstructionCache(GetCurrentProcess(), at, size)) + if (const int e = protect(region, offset, size, PAGE_EXECUTE_READ); e != kal_ok) return e; + if (!FlushInstructionCache(GetCurrentProcess(), static_cast(region) + offset, size)) return okw::translate_win32(GetLastError()); return kal_ok; } -// A PART THAT IS A FILE'S (0.21) IS NOT PROVIDED, AND KAL_EXEC_PROP_FILE IS -// WITHHELD. This system maps a view of a file only where nothing is: a part of -// a reservation becomes a view (MapViewOfFile3, MEM_REPLACE_PLACEHOLDER) only -// where the reservation was made of placeholders and the part is a placeholder -// of its own, split from it before anything was committed there. A region -// would then be a set of pieces, each released by its own call, and this -// implementation would keep the table of them (clause 7.1). A loader copies the -// file's bytes, as it does where the position is zero. -int kal_exec_map_part(void*, kal_uintptr, kal_uintptr, kal_file, kal_u64, kal_uintptr) { - return kal_err_not_supported; +// READ-ONLY AFTER RELOCATION (0.23). KAL_EXEC_PROP_SEAL_FOREVER is not +// claimed: this system's protection of a view or an allocation can always be +// changed again by the program that holds it, so a part asked to be sealed for +// ever is made read-only, as exec.h states for an implementation that does not +// claim the position. +int kal_exec_seal_part(void* region, kal_uintptr offset, kal_uintptr size, kal_uintptr flags) { + const kal_uintptr g = kal_exec_granularity(); + if (region == nullptr || size == 0 || (offset & (g - 1)) != 0 || (size & (g - 1)) != 0 + || (reinterpret_cast(region) & (g - 1)) != 0 || (flags & ~KAL_EXEC_SEAL_FOREVER) != 0) + return kal_err_invalid; + return protect(region, offset, size, PAGE_READONLY); +} + +// A PART THAT IS A FILE'S (0.21): see the head of this file. +int kal_exec_map_part(void* base, kal_uintptr offset, kal_uintptr size, kal_file file, kal_u64 at, + kal_uintptr how) { + const kal_uintptr g = kal_exec_granularity(); + constexpr kal_uintptr known = KAL_EXEC_MAP_EXECUTE | KAL_EXEC_MAP_PREFETCH | KAL_EXEC_MAP_BTI | KAL_EXEC_MAP_WRITE; + if (base == nullptr || size == 0 || (offset & (g - 1)) != 0 || (size & (g - 1)) != 0 + || at % KAL_MAP_ALIGNMENT != 0 || (how & ~known) != 0 + || ((how & KAL_EXEC_MAP_WRITE) && (how & KAL_EXEC_MAP_EXECUTE))) + return kal_err_invalid; + void* fh = okw::unpack(file.h); + if (!fh) return kal_err_invalid; + const calls& c = placeholders(); + if (!c.usable) return kal_err_not_supported; + + okw::io_status_block s{}; + unsigned long access = 0; + if (!okw::ok(okw::NtQueryInformationFile(fh, &s, &access, sizeof access, okw::file_access_information_class))) + return kal_err_invalid; + if ((access & FILE_READ_DATA) == 0) return kal_err_permission; + okw::file_standard_information standard{}; + const long q = okw::NtQueryInformationFile(fh, &s, &standard, sizeof standard, + okw::file_standard_information_class); + if (!okw::ok(q)) return okw::translate_nt(q); + const kal_u64 length = standard.end_of_file > 0 ? static_cast(standard.end_of_file) : 0; + if (at > length || size > length - at) return kal_err_invalid; + if (GetFileType(fh) != FILE_TYPE_DISK) return kal_err_not_supported; + // Views are placed in this system's sixty-four kibibytes. + if (offset % kView != 0 || size % kView != 0) return kal_err_not_supported; + + // The file's section, executable where asked: a file whose access does not + // let it be executed is a property of the file. One written privately is a + // section of copies on write (PAGE_WRITECOPY), which never changes the file. + const bool execute = (how & KAL_EXEC_MAP_EXECUTE) != 0; + const bool written = (how & KAL_EXEC_MAP_WRITE) != 0; + const DWORD view_prot = execute ? PAGE_EXECUTE_READ : written ? PAGE_WRITECOPY : PAGE_READONLY; + void* xh = reopen(fh, FILE_READ_DATA | (execute ? FILE_EXECUTE_ : 0u)); + if (!xh) return execute ? kal_err_not_supported : kal_err_permission; + HANDLE fsec = CreateFileMappingW(xh, nullptr, view_prot, 0, 0, nullptr); + const DWORD why = fsec ? 0 : GetLastError(); + okw::NtClose(xh); + if (!fsec) return why == ERROR_ACCESS_DENIED ? kal_err_not_supported : okw::translate_win32(why); + + const HANDLE self = GetCurrentProcess(); + AcquireSRWLockExclusive(&g_regions_lock); + int e = kal_ok; + region* r = find(base); + unsigned k = 0; + if (!r || offset + size > r->size) e = kal_err_invalid; + // The view of the section that holds the whole part: a part already a + // file's, or one across two views, is refused (a part is made a file's once). + if (e == kal_ok) { + while (k < r->count && !(r->pieces[k].off <= offset && offset < r->pieces[k].off + r->pieces[k].len)) ++k; + if (k == r->count || r->pieces[k].file || offset + size > r->pieces[k].off + r->pieces[k].len) + e = kal_err_invalid; + } + if (e == kal_ok && r->count + 2 > r->cap && !grow(*r)) e = kal_err_no_memory; + runs left, middle, right; + const okw_uptr p0 = e == kal_ok ? r->pieces[k].off : 0; + const okw_uptr p1 = e == kal_ok ? p0 + r->pieces[k].len : 0; + if (e == kal_ok && !(left.take(r->base, p0, offset) && middle.take(r->base, offset, offset + size) + && right.take(r->base, offset + size, p1))) + e = kal_err_not_supported; + if (e == kal_ok && !c.unmap2(self, r->base + p0, MEM_PRESERVE_PLACEHOLDER_)) + e = okw::translate_win32(GetLastError()); + if (e != kal_ok) { + ReleaseSRWLockExclusive(&g_regions_lock); + CloseHandle(fsec); + return e; + } + // From here the region's addresses are placeholders until each is a view + // again; the section holds what they held. + if (p0 < offset) VirtualFree(r->base + p0, offset - p0, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER_); + if (offset + size < p1) VirtualFree(r->base + offset, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER_); + auto own = [&](okw_uptr off, okw_uptr len) { return len == 0 || section_view(c, r->section, r->base + off, off, len); }; + const bool before = own(p0, offset - p0); + bool part = c.map3(fsec, self, r->base + offset, at, size, MEM_REPLACE_PLACEHOLDER_, + view_prot, nullptr, 0) == r->base + offset; + const DWORD part_why = part ? 0 : GetLastError(); + // A file that could not be mapped leaves the part what it was. + if (!part) own(offset, size); + const bool after = own(offset + size, p1 - offset - size); + left.give(r->base); + if (!part) middle.give(r->base); + right.give(r->base); + // The table: the view that was, as the three it is now. + piece pieces[3]; + unsigned n = 0; + if (p0 < offset) pieces[n++] = piece{ p0, offset - p0, false, 0, false }; + pieces[n++] = piece{ offset, size, part, part ? at : 0, part && written }; + if (offset + size < p1) pieces[n++] = piece{ offset + size, p1 - offset - size, false, 0, false }; + for (unsigned i = r->count; i > k + 1; --i) r->pieces[i + n - 2] = r->pieces[i - 1]; + for (unsigned i = 0; i < n; ++i) r->pieces[k + i] = pieces[i]; + r->count += n - 1; + ReleaseSRWLockExclusive(&g_regions_lock); + CloseHandle(fsec); + if (!before || !after) return kal_err_no_memory; + if (!part) return part_why == ERROR_ACCESS_DENIED ? kal_err_not_supported : okw::translate_win32(part_why); + // THE PAGES READ IN NOW (0.23): one request for the part's pages rather + // than a fault for each. A request about time; a system without the call + // maps the part as without it. + if ((how & KAL_EXEC_MAP_PREFETCH) && c.prefetch) { + WIN32_MEMORY_RANGE_ENTRY_ range{ r->base + offset, size }; + c.prefetch(self, 1, &range, 0); + } + return kal_ok; } +// KAL_EXEC_MAP_BTI is accepted and not enforced: this system's processors +// mark no branch targets for it to hold a program to. kal_uintptr kal_exec_props(void) { - return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE; + return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE | KAL_EXEC_PROP_PLACE + | (placeholders().usable ? KAL_EXEC_PROP_FILE : 0u); } } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index 8db19e2..6e6b722 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -1,6 +1,8 @@ #include "win.h" #include "endpoint.h" // whether the local family exists (0.17) #include "handle.h" +#include "rights.h" +#include #include #include #include @@ -28,6 +30,20 @@ namespace { constexpr okw_u64 kEpochDifference = 116444736000000000ull; +// THE RIGHTS OF A DIRECTORY HANDLE (0.23, rights.h): an operation that would +// change something beneath a read-only handle is refused before it is asked of +// the system; one that did change something is recorded, and what a handle that +// states nothing changes has listed no longer holds. +inline bool refused_write(void* b) { return (okw::rights::of(b) & okw::rights::read_only) != 0; } +inline void wrote(void* b, const char* n, kal_uintptr l) { + if (!okw::rights::g_any) return; + okw::rights::changed(b); + okw::rights::note(b, 'w', n, l); +} +inline void asked(void* b, bool present, const char* n, kal_uintptr l) { + if (okw::rights::g_any) okw::rights::note(b, present ? 's' : 'a', n, l); +} + kal_duration to_nanoseconds(okw_i64 ticks) { const okw_u64 t = static_cast(ticks); return t < kEpochDifference ? 0u : (t - kEpochDifference) * 100ull; @@ -73,6 +89,119 @@ void* open_nt_directory(const wchar_t* nt, unsigned short bytes) { SRWLOCK_ g_table_lock{}; DWORD g_drives_seen = 0; +bool g_granted = false; + +// THE DIRECTORIES A STARTER GRANTED, WHEN THE PROGRAM WAS STARTED WITH GRANTS +// (0.23). +// +// They arrive as handles this system placed in the program at the values they +// had in the starter's duplicates, and their names in the variable +// `kal_process_spawn' writes (process.cpp): +// +// KAL_PREOPENS={;,,} +// +// read only when `' is the program that started this one, so that a value +// inherited through a program that does not read it --- a shell --- names no one +// one generation further down. A handle that is not a directory is reported as +// a preopen this program may not use. Each is made uninheritable as it is +// taken over, so that a grant reaches the program it was made to and not that +// program's own children (SPEC.md clause 7.13). +// +// WHAT A GRANT CARRIES travels beside it, read under the same condition: +// +// KAL_GRANT_RIGHTS={;,,,,} +// +// Answers false when there is no such value, which leaves the directories this +// implementation supplies by default. +okw_uptr parent_id() { + okw_uptr basic[6] = {}; + if (!okw::ok(::NtQueryInformationProcess(GetCurrentProcess(), 0 /* ProcessBasicInformation */, + basic, sizeof basic, nullptr))) + return 0; + return basic[5]; // InheritedFromUniqueProcessId +} + +// The value of a variable this program was started with, in storage of its own. +struct variable { + char* v = nullptr; okw_uptr n = 0; + explicit variable(const char* name) { + const kal_intptr len = kal_env_var(name, okw::length(name), nullptr, 0); + if (len < 0) return; + n = static_cast(len); + v = static_cast(kal_alloc(n + 1, 1)); + if (!v) { n = 0; return; } + kal_env_var(name, okw::length(name), v, n); + v[n] = '\0'; + } + ~variable() { if (v) kal_free(v, n + 1, 1); } +}; + +bool number(const char*& v, long long& out) { + bool neg = false; + if (*v == '-') { neg = true; ++v; } + if (*v < '0' || *v > '9') return false; + out = 0; + while (*v >= '0' && *v <= '9') out = out * 10 + (*v++ - '0'); + if (neg) out = -out; + return true; +} + +bool for_this_program(const char*& v) { + long long pid = 0; + return number(v, pid) && pid != 0 && static_cast(pid) == parent_id(); +} + +void adopt_rights(const preopen* t, kal_uintptr count) { + variable var("KAL_GRANT_RIGHTS"); + const char* v = var.v; + if (v == nullptr || !for_this_program(v)) return; + while (*v == ';') { + ++v; + long long index = 0, flags = 0, record = -1, len = 0; + if (!number(v, index) || *v++ != ',' || !number(v, flags) || *v++ != ',' || !number(v, record) + || *v++ != ',' || !number(v, len) || *v++ != ',' || len < 0) + return; + for (long long i = 0; i < len; ++i) if (v[i] == '\0') return; + if (index >= 0 && static_cast(index) < count && t[index].handle != 0) + okw::rights::adopt(okw::unpack(t[index].handle), static_cast(flags), + record < 0 ? nullptr : reinterpret_cast(static_cast(record)), + v, static_cast(len)); + v += len; + } +} + +bool granted(preopen* t, kal_uintptr* n) { + variable var("KAL_PREOPENS"); + const char* v = var.v; + if (v == nullptr || !for_this_program(v)) return false; + kal_uintptr k = 0; + while (*v == ';' && k < kMaxPreopens) { + ++v; + long long value = 0, len = 0; + if (!number(v, value) || *v++ != ',' || !number(v, len) || *v++ != ',' || len < 0 + || static_cast(len) >= okw::kMaxName) + return false; + for (long long i = 0; i < len; ++i) if (v[i] == '\0') return false; + void* h = reinterpret_cast(static_cast(value)); + okw::io_status_block s{}; + okw::file_standard_information standard{}; + const bool dir = value > 0 + && okw::ok(okw::NtQueryInformationFile(h, &s, &standard, sizeof standard, + okw::file_standard_information_class)) + && standard.directory; + if (dir) SetHandleInformation(h, HANDLE_FLAG_INHERIT, 0); + for (long long i = 0; i < len; ++i) t[k].name[i] = v[i]; + t[k].name[len] = '\0'; + t[k].len = static_cast(len); + t[k].handle = dir ? okw::pack(h) : 0u; + ++k; + v += len; + } + if (*v != '\0') return false; + *n = k; + adopt_rights(t, k); + return true; +} preopen* table(kal_uintptr* count) { static preopen t[kMaxPreopens]; @@ -80,8 +209,9 @@ preopen* table(kal_uintptr* count) { static bool opened = false; // A VOLUME MOUNTED SINCE INCEPTION IS APPENDED (K9, openkal 0.17: a // position once given keeps its directory). The system's map of drive - // letters is one word, so asking whether one appeared costs one call. - if (opened && GetLogicalDrives() != g_drives_seen) { + // letters is one word, so asking whether one appeared costs one call. A + // program started with grants has those and nothing else. + if (opened && !g_granted && GetLogicalDrives() != g_drives_seen) { AcquireSRWLockExclusive(&g_table_lock); const DWORD now = GetLogicalDrives(); for (int d = 0; d < 26 && used < kMaxPreopens; ++d) { @@ -100,6 +230,11 @@ preopen* table(kal_uintptr* count) { g_drives_seen = now; ReleaseSRWLockExclusive(&g_table_lock); } + if (!opened) { + AcquireSRWLockExclusive(&g_table_lock); + if (!opened && granted(t, &used)) { g_granted = true; opened = true; } + ReleaseSRWLockExclusive(&g_table_lock); + } if (!opened) { opened = true; g_drives_seen = GetLogicalDrives(); @@ -215,19 +350,60 @@ kal_uintptr put_name(const char* src, kal_uintptr n, return n; } +// THE RECORDS OF A NODE THIS SYSTEM GIVES IN ONE REQUEST (Windows 10 1709; +// the second, with the volume, 11 24H2): by handle, and by name without one +// (`info_by_name' below). +struct file_stat_information { + okw_i64 file_id, creation_time, last_access_time, last_write_time, change_time, allocation_size, end_of_file; + unsigned long attributes, reparse_tag, number_of_links, effective_access; +}; +struct file_stat_basic_information { + okw_i64 file_id, creation_time, last_access_time, last_write_time, change_time, allocation_size, end_of_file; + unsigned long attributes, reparse_tag, number_of_links, device_type, device_characteristics, reserved; + okw_i64 volume_serial_number; + unsigned char file_id_128[16]; +}; +static_assert(sizeof(file_stat_information) == 72, "FILE_STAT_INFORMATION"); +static_assert(sizeof(file_stat_basic_information) == 104, "FILE_STAT_BASIC_INFORMATION"); +constexpr int file_stat_information_class = 68; +constexpr int file_stat_basic_information_class = 77; +bool class_refused(long r) { + return r == static_cast(0xC0000003) /* INVALID_INFO_CLASS */ || r == static_cast(0xC000000D) + || r == static_cast(0xC00000BB) /* NOT_SUPPORTED */ || r == okw::status_not_implemented; +} +volatile long long g_stat_by_handle = 1; // cleared where the system has not the record + // Writes no more of the structure than the caller says exists on its side, and -// reports which fields it filled. +// reports which fields it filled. The node's record in one request where the +// system gives it, and the basic and standard records otherwise. int fill(void* h, kal_u32 wanted, kal_node_info* out) { (void)wanted; okw::io_status_block s{}; okw::file_basic_information basic{}; okw::file_standard_information standard{}; - long r = okw::NtQueryInformationFile(h, &s, &basic, sizeof basic, - okw::file_basic_information_class); - if (!okw::ok(r)) return okw::translate_nt(r); - r = okw::NtQueryInformationFile(h, &s, &standard, sizeof standard, - okw::file_standard_information_class); - if (!okw::ok(r)) return okw::translate_nt(r); + okw_i64 file_id = 0; + bool stat = false; + if (okw::atomic::load(&g_stat_by_handle)) { + file_stat_information st{}; + const long q = okw::NtQueryInformationFile(h, &s, &st, sizeof st, file_stat_information_class); + if (okw::ok(q)) { + basic.last_write_time = st.last_write_time; + basic.attributes = st.attributes; + standard.end_of_file = st.end_of_file; + file_id = st.file_id; + stat = true; + } else if (q == static_cast(0xC0000003) || q == okw::status_not_implemented) { + okw::atomic::store(&g_stat_by_handle, 0); + } + } + if (!stat) { + long r = okw::NtQueryInformationFile(h, &s, &basic, sizeof basic, + okw::file_basic_information_class); + if (!okw::ok(r)) return okw::translate_nt(r); + r = okw::NtQueryInformationFile(h, &s, &standard, sizeof standard, + okw::file_standard_information_class); + if (!okw::ok(r)) return okw::translate_nt(r); + } const kal_u32 self = out->self_size; kal_node_info v{}; @@ -273,9 +449,9 @@ int fill(void* h, kal_u32 wanted, kal_node_info* out) { okw::file_fs_volume_information info; wchar_t label_tail[128]; } volume{}; - okw::file_internal_information index{}; - const long ri = okw::NtQueryInformationFile(h, &s, &index, sizeof index, - okw::file_internal_information_class); + okw::file_internal_information index{ file_id }; + const long ri = stat ? 0 : okw::NtQueryInformationFile(h, &s, &index, sizeof index, + okw::file_internal_information_class); const long rv = okw::NtQueryVolumeInformationFile(h, &s, &volume, sizeof volume, okw::fs_volume_information_class); if (okw::ok(ri) && (okw::ok(rv) || rv == okw::status_buffer_overflow)) { @@ -312,6 +488,14 @@ struct listing { okw_uptr used; okw_uptr at; bool first; + // THE CLASS OF THE LISTING (0.23): FileIdFullDirectoryInformation, whose + // records carry the entry's size, times, attributes and identity, so that a + // listing that asks for them asks nothing more; FileDirectoryInformation on + // a volume that refuses the first, which carries all but the identity. The + // volume's serial number is the identity's other word (`fill'). + int cls; + bool have_volume; + okw_u64 volume; // Aligned deliberately. The records this environment writes here begin with // fields it reads as machine words, and it requires the buffer they are // written into to be aligned for them. Placed after three words and a @@ -413,10 +597,26 @@ int kal_fs_preopen(kal_uintptr index, kal_dir* out, int kal_fs_open_dir(kal_dir base, const char* name, kal_uintptr len, kal_dir* out) { void* root = dir_handle(base); if (!root || out == nullptr || !okw::acceptable(name, len)) return kal_err_invalid; + const okw_u32 rights = okw::rights::of(root); + if ((rights & okw::rights::unchanging) && okw::rights::known_absent(root, name, len)) { + asked(root, false, name, len); + return kal_err_not_found; + } void* h = nullptr; const long r = open_relative(root, name, len, FILE_LIST_DIRECTORY | GENERIC_READ, okw::file_open, okw::file_directory_file, &h); - if (!okw::ok(r)) return okw::translate_nt(r); + if (!okw::ok(r)) { + const int e = okw::translate_nt(r); + if (rights && (e == kal_err_not_found || e == kal_err_not_directory)) asked(root, false, name, len); + return e; + } + // A directory opened beneath a handle with rights has them; where the table + // has no room the directory is not handed out with more than its base. + if (rights && !okw::rights::derive(root, h, name, len)) { + okw::NtClose(h); + return kal_err_no_memory; + } + if (rights) asked(root, true, name, len); *out = kal_dir{ okw::pack(h) }; return kal_ok; } @@ -483,10 +683,25 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, else if (truncate) disposition = okw::file_overwrite; else disposition = okw::file_open; + const okw_u32 rights = okw::rights::of(root); + const bool changes = (flags & (KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE | KAL_OPEN_APPEND)) != 0; + if (changes && (rights & okw::rights::read_only)) return kal_err_permission; + if (!create && (rights & okw::rights::unchanging) && okw::rights::known_absent(root, name, len)) { + asked(root, false, name, len); + return kal_err_not_found; + } void* h = nullptr; const long r = open_relative(root, name, len, access, disposition, okw::file_non_directory_file, &h); - if (!okw::ok(r)) return okw::translate_nt(r); + if (!okw::ok(r)) { + const int e = okw::translate_nt(r); + if (rights && (e == kal_err_not_found || e == kal_err_not_directory)) asked(root, false, name, len); + return e; + } + if (rights) { + if (changes) wrote(root, name, len); + else okw::rights::note(root, 'r', name, len); + } *out = kal_file{ okw::pack(h) }; return kal_ok; } @@ -497,6 +712,7 @@ void kal_fs_close_dir(kal_dir d) { // Before the close, because after it the same value may already name // another directory whose enumerations are not this one's. listings_end(h); + okw::rights::forget(h); okw::retire(d.h); okw::NtClose(h); } @@ -648,10 +864,132 @@ int kal_fs_truncate(kal_file f, kal_u64 size) { return okw::ok(r) ? kal_ok : okw::translate_nt(r); } +// AN ENQUIRY BY NAME IS ONE REQUEST WHERE THIS SYSTEM HAS ONE (0.19.0). +// +// Answered by opening the node, it is six: the open, four enquiries of the +// handle --- the basic and standard records, the index, the volume --- and the +// close. NtQueryInformationByName (Windows 10 1709) answers the record of all +// but the volume for a name, without a handle; Windows 11 24H2 adds a record +// that holds the volume's serial number as well. So: +// +// FileStatBasicInformation one request, everything; +// FileStatInformation one request where the identity is not wanted; +// two, with the base directory's volume, where the +// name is one component and so cannot lie on +// another volume; and otherwise the open below, +// because a name of several components may pass +// through a directory where another volume is +// mounted, whose serial is not the base's. +// +// Either record describes the node the name ends at and does not follow it: a +// node whose content is another name, asked about by a caller who wants it +// resolved, is asked about by opening. A volume that refuses the record is +// asked by opening; a system without the class is not asked again. +namespace { + +volatile long long g_by_name = -1; // -1 not asked; 0 none; 1 the stat record; 2 the basic one too +pfn_NtQueryInformationByName_ g_query_by_name = nullptr; + + +// kal_ok or a kal_err when answered; 1 when the node is to be opened instead. +int info_by_name(void* root, const char* name, kal_uintptr len, kal_uintptr flags, kal_u32 wanted, + kal_node_info* out, bool& absent) { + long long have = okw::atomic::load(&g_by_name); + if (have < 0) { + g_query_by_name = reinterpret_cast( + okw::system_function(L"ntdll.dll", "NtQueryInformationByName")); + have = g_query_by_name ? 2 : 0; + okw::atomic::store(&g_by_name, have); + } + if (have == 0) return 1; + okw::wide_name w(name, len); + if (!w.ok) return 1; + okw::object_attributes attrs{}; + attrs.length = sizeof attrs; + attrs.root_directory = root; + attrs.object_name = &w.string; + attrs.attributes = okw::obj_case_insensitive; + okw::io_status_block status{}; + file_stat_basic_information b{}; + bool basic = false; + long r = 0; + if (have == 2) { + r = g_query_by_name(&attrs, &status, &b, sizeof b, file_stat_basic_information_class); + if (r == static_cast(0xC0000003) || r == okw::status_not_implemented) { + okw::atomic::store(&g_by_name, 1); // this system has not the class + } else if (okw::ok(r)) { + basic = true; + } else if (!class_refused(r)) { + const int e = okw::translate_nt(r); + if (e == kal_err_not_found || e == kal_err_not_directory) { absent = true; return kal_ok; } + return 1; + } + } + if (!basic) { + file_stat_information st{}; + r = g_query_by_name(&attrs, &status, &st, sizeof st, file_stat_information_class); + if (!okw::ok(r)) { + if (class_refused(r)) return 1; + const int e = okw::translate_nt(r); + if (e == kal_err_not_found || e == kal_err_not_directory) { absent = true; return kal_ok; } + return 1; + } + b.file_id = st.file_id; b.last_write_time = st.last_write_time; b.end_of_file = st.end_of_file; + b.attributes = st.attributes; b.reparse_tag = st.reparse_tag; + } + const bool link = (b.attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + if (link && !(flags & KAL_FS_NO_RESOLVE)) return 1; + // The volume's serial number, where the record did not give it. + bool one_component = true; + for (kal_uintptr i = 0; i < len; ++i) if (name[i] == '/' || name[i] == '\\') one_component = false; + okw_u64 serial = basic ? static_cast(b.volume_serial_number) : 0; + bool have_serial = basic; + if (!basic && (wanted & KAL_INFO_IDENTITY)) { + if (!one_component) return 1; + struct { okw::file_fs_volume_information info; wchar_t label_tail[128]; } volume{}; + okw::io_status_block vs{}; + const long rv = okw::NtQueryVolumeInformationFile(root, &vs, &volume, sizeof volume, + okw::fs_volume_information_class); + have_serial = okw::ok(rv) || rv == okw::status_buffer_overflow; + serial = volume.info.serial_number; + } + const kal_u32 self = out->self_size; + kal_node_info v{}; + v.self_size = self; + v.present = KAL_INFO_KIND | KAL_INFO_SIZE | KAL_INFO_MODIFIED | KAL_INFO_WRITABLE; + v.size = static_cast(b.end_of_file); + v.modified_ns = to_nanoseconds(b.last_write_time); + v.kind = link ? kal_node_link : (b.attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory : kal_node_file; + v.writable = (b.attributes & FILE_ATTRIBUTE_READONLY) ? 0 : 1; + if (have_serial && b.file_id != 0 && (wanted & KAL_INFO_IDENTITY)) { + v.identity[0] = serial; + v.identity[1] = static_cast(b.file_id); + v.present |= KAL_INFO_IDENTITY; + } + put_bytes(out, &v, self < sizeof v ? self : static_cast(sizeof v)); + return kal_ok; +} +} // namespace + int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_uintptr flags, kal_u32 wanted, kal_node_info* out) { void* root = dir_handle(base); if (!root || !info_ok(out) || !okw::acceptable(name, len)) return kal_err_invalid; + const okw_u32 rights = okw::rights::of(root); + if ((rights & okw::rights::unchanging) && okw::rights::known_absent(root, name, len)) { + asked(root, false, name, len); + fill_absent(out); + return kal_ok; + } + { + bool absent = false; + const int e = info_by_name(root, name, len, flags, wanted, out, absent); + if (e != 1) { + if (absent) fill_absent(out); + if (e == kal_ok && rights) asked(root, !absent, name, len); + return e; + } + } void* h = nullptr; // RESOLVES BY DEFAULT, SO THAT ASKING AND OPENING ANSWER THE SAME QUESTION. // Without FILE_OPEN_REPARSE_POINT this environment follows the node to what @@ -668,11 +1006,13 @@ int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, // something absent, when the enquiry resolves. const int e = okw::translate_nt(r); if (e == kal_err_not_found || e == kal_err_not_directory) { + if (rights) asked(root, false, name, len); fill_absent(out); return kal_ok; } return e; } + if (rights) asked(root, true, name, len); const int e = fill(h, wanted, out); okw::NtClose(h); return e; @@ -710,6 +1050,7 @@ int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, kal_u64 modified_ns) { void* root = dir_handle(base); if (!root || !okw::acceptable(name, len)) return kal_err_invalid; + if (refused_write(root)) return kal_err_permission; void* h = nullptr; const long r = open_relative(root, name, len, FILE_WRITE_ATTRIBUTES, @@ -724,7 +1065,9 @@ int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, const long w = okw::NtSetInformationFile(h, &iosb, &basic, sizeof basic, okw::file_basic_information_class); okw::NtClose(h); - return okw::ok(w) ? kal_ok : okw::translate_nt(w); + if (!okw::ok(w)) return okw::translate_nt(w); + wrote(root, name, len); + return kal_ok; } // Whether a NAME's node may be started, version 0.13. @@ -817,17 +1160,20 @@ int kal_fs_capacity(kal_dir d, kal_u64* total, kal_u64* available) { int kal_fs_mkdir(kal_dir base, const char* name, kal_uintptr len) { void* root = dir_handle(base); if (!root || !okw::acceptable(name, len)) return kal_err_invalid; + if (refused_write(root)) return kal_err_permission; void* h = nullptr; const long r = open_relative(root, name, len, FILE_LIST_DIRECTORY, okw::file_create, okw::file_directory_file, &h); if (!okw::ok(r)) return okw::translate_nt(r); okw::NtClose(h); + wrote(root, name, len); return kal_ok; } int kal_fs_remove(kal_dir base, const char* name, kal_uintptr len) { void* root = dir_handle(base); if (!root || !okw::acceptable(name, len)) return kal_err_invalid; + if (refused_write(root)) return kal_err_permission; void* h = nullptr; // One operation removes a name, and this environment distinguishes two // kinds of name where the interface does not, so neither kind is asked for. @@ -863,7 +1209,9 @@ int kal_fs_remove(kal_dir base, const char* name, kal_uintptr len) { okw::file_disposition_information_class); } okw::NtClose(h); - return okw::ok(d) ? kal_ok : okw::translate_nt(d); + if (!okw::ok(d)) return okw::translate_nt(d); + wrote(root, name, len); + return kal_ok; } static int rename_with(kal_dir from, const char* a, kal_uintptr alen, @@ -872,6 +1220,7 @@ static int rename_with(kal_dir from, const char* a, kal_uintptr alen, void* target_root = dir_handle(to); if (!source_root || !target_root) return kal_err_invalid; if (!okw::acceptable(a, alen) || !okw::acceptable(b, blen)) return kal_err_invalid; + if (refused_write(source_root) || refused_write(target_root)) return kal_err_permission; void* h = nullptr; // The name is renamed, a link included, never what it leads to. @@ -907,7 +1256,10 @@ static int rename_with(kal_dir from, const char* a, kal_uintptr alen, kal_free(info, storage_bytes, alignof(okw::file_rename_information)); if (!replace && n == static_cast(0xC0000035) /* STATUS_OBJECT_NAME_COLLISION */) return kal_err_exists; - return okw::ok(n) ? kal_ok : okw::translate_nt(n); + if (!okw::ok(n)) return okw::translate_nt(n); + wrote(source_root, a, alen); + wrote(target_root, b, blen); + return kal_ok; } int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, @@ -944,16 +1296,27 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { if (!s) { okw::NtClose(own); return kal_err_no_memory; } s->handle = own; s->dir = root; s->busy = 0; s->doomed = 0; s->used = 0; s->at = 0; s->first = true; + s->cls = okw::file_id_full_directory_information_class; + { + struct { okw::file_fs_volume_information info; wchar_t label_tail[128]; } volume{}; + okw::io_status_block vs{}; + const long rv = okw::NtQueryVolumeInformationFile(own, &vs, &volume, sizeof volume, + okw::fs_volume_information_class); + s->have_volume = okw::ok(rv) || rv == okw::status_buffer_overflow; + s->volume = s->have_volume ? static_cast(volume.info.serial_number) : 0; + } AcquireSRWLockExclusive(&g_listings_lock); s->next = g_listings; g_listings = s; ReleaseSRWLockExclusive(&g_listings_lock); *iter = reinterpret_cast(s); + if (okw::rights::g_any) okw::rights::note(root, 'l', "", 0); return kal_ok; } -int kal_fs_list_next(kal_dir d, kal_uintptr* iter, - char* name_out, kal_uintptr name_cap, - kal_uintptr* name_len, int* kind) { +namespace { +// One entry of a listing, and what the record of it told (`info', where given). +int list_step(kal_dir d, kal_uintptr* iter, char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, int* kind, kal_u32 wanted, kal_node_info* info) { // A closed directory is a released handle (clause 6.7), and closing it // released the iterator too, which fs.h states. void* root = dir_handle(d); @@ -964,15 +1327,26 @@ int kal_fs_list_next(kal_dir d, kal_uintptr* iter, for (;;) { if (s->at >= s->used) { okw::io_status_block status{}; - const long r = okw::NtQueryDirectoryFile( + long r = okw::NtQueryDirectoryFile( s->handle, nullptr, nullptr, nullptr, &status, s->buffer, static_cast(sizeof s->buffer), - okw::file_directory_information_class, 0, nullptr, s->first ? 1 : 0); + s->cls, 0, nullptr, s->first ? 1 : 0); + // A volume that does not give an entry's identity in its listing + // refuses the class, and is listed by the one before it. + if (s->first && s->cls == okw::file_id_full_directory_information_class + && (r == static_cast(0xC0000003) || r == static_cast(0xC000000D) + || r == static_cast(0xC00000BB) || r == okw::status_not_implemented)) { + s->cls = okw::file_directory_information_class; + r = okw::NtQueryDirectoryFile(s->handle, nullptr, nullptr, nullptr, &status, + s->buffer, static_cast(sizeof s->buffer), + s->cls, 0, nullptr, 1); + } s->first = false; if (!okw::ok(r)) { listing_give(s, true); *iter = 0; if (name_len) *name_len = 0; + if (info) fill_absent(info); // The end of a directory is reported here as a distinct status // and is the ordinary outcome, not a failure. return (static_cast(r) == 0x80000006u) ? kal_ok @@ -982,7 +1356,12 @@ int kal_fs_list_next(kal_dir d, kal_uintptr* iter, s->at = 0; } + // The two classes agree up to the length of the name; the name follows + // the identity in the second. auto* e = reinterpret_cast(s->buffer + s->at); + const bool full = s->cls == okw::file_id_full_directory_information_class; + const auto* x = reinterpret_cast(e); + const wchar_t* wname = full ? x->file_name : e->file_name; const okw_uptr step = e->next_entry_offset ? e->next_entry_offset : (s->used - s->at); s->at += step; @@ -991,18 +1370,86 @@ int kal_fs_list_next(kal_dir d, kal_uintptr* iter, const okw_uptr chars = e->file_name_length / 2; // The two entries that name the directory and its parent are omitted. // They exist to support ascent, which this interface does not offer. - if (chars == 1 && e->file_name[0] == L'.') continue; - if (chars == 2 && e->file_name[0] == L'.' && e->file_name[1] == L'.') continue; + if (chars == 1 && wname[0] == L'.') continue; + if (chars == 2 && wname[0] == L'.' && wname[1] == L'.') continue; - const okw_uptr n = okw::narrow(e->file_name, chars, s->reported, sizeof s->reported); + const okw_uptr n = okw::narrow(wname, chars, s->reported, sizeof s->reported); put_name(s->reported, n, name_out, name_cap, name_len); - if (kind) *kind = (e->file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link - : (e->file_attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory - : kal_node_file; + const int k = (e->file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link + : (e->file_attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory + : kal_node_file; + if (kind) *kind = k; + if (info) { + // What the record carries, of the entry itself: a link is described + // as the link, as the listing found it. + kal_node_info v{}; + v.self_size = info->self_size; + v.kind = k; + v.present = KAL_INFO_KIND; + if (wanted & KAL_INFO_SIZE) { v.size = static_cast(e->end_of_file); v.present |= KAL_INFO_SIZE; } + if (wanted & KAL_INFO_MODIFIED) { + v.modified_ns = to_nanoseconds(e->last_write_time); + v.present |= KAL_INFO_MODIFIED; + } + if (wanted & KAL_INFO_WRITABLE) { + v.writable = (e->file_attributes & FILE_ATTRIBUTE_READONLY) ? 0 : 1; + v.present |= KAL_INFO_WRITABLE; + } + if ((wanted & KAL_INFO_IDENTITY) && full && s->have_volume && x->file_id != 0) { + v.identity[0] = s->volume; + v.identity[1] = static_cast(x->file_id); + v.present |= KAL_INFO_IDENTITY; + } + const kal_u32 self = info->self_size; + put_bytes(info, &v, self < sizeof v ? self : static_cast(sizeof v)); + } listing_give(s, false); return kal_ok; } } +} // namespace + +int kal_fs_list_next(kal_dir d, kal_uintptr* iter, + char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, int* kind) { + return list_step(d, iter, name_out, name_cap, name_len, kind, 0, nullptr); +} + +// THE LISTING WITH WHAT IT TOLD OF THE ENTRY (0.23): one request returns a +// batch of records, each with the entry's size, times, attributes and identity. +int kal_fs_list_next_info(kal_dir d, kal_uintptr* iter, char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, kal_u32 wanted, kal_node_info* info) { + if (!info_ok(info)) return kal_err_invalid; + return list_step(d, iter, name_out, name_cap, name_len, nullptr, wanted, info); +} + +// A HANDLE WITH LESS (0.23, rights.h). +int kal_fs_dir_attenuate(kal_dir d, const kal_dir_rights* rights, kal_dir* out) { + void* h = dir_handle(d); + if (!h || rights == nullptr || out == nullptr) return kal_err_invalid; + // The fields before `record': two words of thirty-two bits and a flags word. + if (rights->self_size < sizeof(kal_u32) * 2 + sizeof(kal_uintptr)) return kal_err_invalid; + constexpr kal_uintptr known = KAL_DIR_READ_ONLY | KAL_DIR_UNCHANGING | KAL_DIR_RECORDED; + if ((rights->flags & ~known) != 0) return kal_err_invalid; + void* record = nullptr; + if (rights->flags & KAL_DIR_RECORDED) { + if (rights->self_size < sizeof(kal_dir_rights)) return kal_err_invalid; + record = file_handle(rights->record); + if (!record) return kal_err_invalid; + // Opened for writing: the record is appended to, not read. + okw::io_status_block st{}; + unsigned long access = 0; + if (!okw::ok(okw::NtQueryInformationFile(record, &st, &access, sizeof access, + okw::file_access_information_class)) + || (access & (FILE_WRITE_DATA | FILE_APPEND_DATA)) == 0) + return kal_err_invalid; + } + void* nh = nullptr; + const int e = okw::rights::attenuate(h, static_cast(rights->flags), record, &nh); + if (e != kal_ok) return e; + *out = kal_dir{ okw::pack(nh) }; + return kal_ok; +} // The properties of the volume a directory is on. // @@ -1070,7 +1517,12 @@ kal_uintptr kal_fs_props(kal_dir d) { struct { okw::file_fs_attribute_information info; wchar_t rest[64]; } a{}; const long r = okw::NtQueryVolumeInformationFile(h, &s, &a, sizeof a, okw::fs_attribute_information_class); - if (!okw::ok(r)) return conservative; + if (!okw::ok(r)) { + const okw_u32 rights = okw::rights::of(h); + return conservative | KAL_FS_PROP_RESOLVED_NAME | KAL_FS_PROP_ATTENUATE + | ((rights & okw::rights::read_only) ? KAL_FS_PROP_READ_ONLY : 0u) + | ((rights & okw::rights::unchanging) ? KAL_FS_PROP_UNCHANGING : 0u); + } kal_uintptr p = conservative; if (a.info.attributes & okw::fs_case_sensitive_search) p |= KAL_FS_PROP_CASE_SENSITIVE; @@ -1104,6 +1556,18 @@ kal_uintptr kal_fs_props(kal_dir d) { // offers the local family. if ((a.info.attributes & okw::fs_supports_reparse_points) && okw::local_family_available()) p |= KAL_FS_PROP_LOCAL_ENDPOINT; + // A name with every link followed (0.23) is what this system answers for + // any handle it has opened (GetFinalPathNameByHandleW). + p |= KAL_FS_PROP_RESOLVED_NAME; + // A handle with less, and what this one carries (0.23, rights.h). + const okw_u32 rights = okw::rights::of(h); + p |= KAL_FS_PROP_ATTENUATE; + if (rights & okw::rights::read_only) p |= KAL_FS_PROP_READ_ONLY; + if (rights & okw::rights::unchanging) p |= KAL_FS_PROP_UNCHANGING; + // A listing gives size, time and identity where the volume keeps an + // identity for each file that it can be opened by --- the one its listing + // reports (0.23). + if (a.info.attributes & okw::fs_supports_open_by_file_id) p |= KAL_FS_PROP_LIST_INFO; return p; } @@ -1188,6 +1652,7 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, return kal_err_invalid; if ((flags & ~static_cast(KAL_FS_LINK_DIRECTORY)) != 0) return kal_err_invalid; if ((kal_fs_props(base) & KAL_FS_PROP_MAKE_LINKS) == 0) return kal_err_not_supported; + if (refused_write(root)) return kal_err_permission; struct scratch { wchar_t dir[okw::kMaxName]; wchar_t link[okw::kMaxName]; wchar_t target[okw::kMaxName]; wchar_t resolved[okw::kMaxName]; @@ -1229,7 +1694,7 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, okw::file_basic_information_class)) && (basic.attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; okw::NtClose(probe); - if (link) return kal_ok; + if (link) { wrote(root, name, len); return kal_ok; } } } if (made) return kal_err_io; @@ -1279,9 +1744,42 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, return okw::translate_win32(e); } okw::NtClose(h); + wrote(root, name, len); return kal_ok; } +// THE NAME WITH EVERY LINK FOLLOWED (0.23): the node opened for its attributes, +// following every reparse point, and the name this system resolved for the +// handle, in a drive's or a share's form (final_name). +kal_intptr kal_fs_resolved_name(kal_dir base, const char* name, kal_uintptr len, + char* out, kal_uintptr cap) { + void* root = dir_handle(base); + if (!root || !okw::acceptable(name, len)) return -kal_err_invalid; + const okw_u32 rights = okw::rights::of(root); + if ((rights & okw::rights::unchanging) && okw::rights::known_absent(root, name, len)) { + asked(root, false, name, len); + return -kal_err_not_found; + } + void* h = nullptr; + const long r = open_relative(root, name, len, FILE_READ_ATTRIBUTES | SYNCHRONIZE, okw::file_open, + okw::file_open_for_backup_intent, &h); + if (!okw::ok(r)) { + const int e = okw::translate_nt(r); + if (rights && (e == kal_err_not_found || e == kal_err_not_directory)) asked(root, false, name, len); + return -e; + } + if (rights) asked(root, true, name, len); + wchar_t wide[okw::kMaxName]; + const okw_uptr wn = final_name(h, wide, okw::kMaxName); + okw::NtClose(h); + if (wn == 0) return -kal_err_io; + char narrow[3 * okw::kMaxName]; + const okw_uptr n = okw::narrow(wide, wn, narrow, sizeof narrow, false); + if (out != nullptr) + for (okw_uptr i = 0; i < n && i < cap; ++i) out[i] = narrow[i]; + return static_cast(n); +} + kal_intptr kal_fs_link_read(kal_dir base, const char* name, kal_uintptr len, char* out, kal_uintptr cap) { void* root = dir_handle(base); diff --git a/src/memory.cpp b/src/memory.cpp index 879bbfd..8380c2b 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -81,4 +81,46 @@ kal_uintptr kal_memory_granularity(void) { return page > grain ? page : grain; } +// WHAT THIS PROGRAM MAY USE, AND WHAT IT CAN BE GIVEN NOW (0.23): the machine's +// memory and what is free of it (GlobalMemoryStatusEx), each no more than a +// limit on the program, or on the job it is in, leaves. +int kal_memory_info(kal_memory_status* out) { + if (out == nullptr || out->self_size < sizeof(kal_u32) * 2) return kal_err_invalid; + kal_memory_status v{}; + v.self_size = out->self_size; + MEMORYSTATUSEX_ m{}; + m.dwLength = sizeof m; + if (GlobalMemoryStatusEx(&m)) { + v.total_bytes = m.ullTotalPhys; + v.available_bytes = m.ullAvailPhys; + v.present = KAL_MSTATUS_TOTAL | KAL_MSTATUS_AVAILABLE; + JOB_LIMITS l{}; + if (QueryInformationJobObject(nullptr, JobObjectExtendedLimitInformation_, &l, sizeof l, nullptr)) { + constexpr DWORD process_memory = 0x100u; // JOB_OBJECT_LIMIT_PROCESS_MEMORY + auto bound = [&](unsigned long long limit, unsigned long long used) { + if (limit == 0) return; + if (limit < v.total_bytes) v.total_bytes = limit; + const unsigned long long left = limit > used ? limit - used : 0; + if (left < v.available_bytes) v.available_bytes = left; + }; + if (l.LimitFlags & process_memory) { + PROCESS_MEMORY_COUNTERS_EX_ c{}; + c.cb = sizeof c; + K32GetProcessMemoryInfo(GetCurrentProcess(), &c, sizeof c); + bound(l.ProcessMemoryLimit, c.PagefileUsage); + } + if (l.LimitFlags & JOB_OBJECT_LIMIT_JOB_MEMORY_) { + JOB_VIOLATION_ j{}; + QueryInformationJobObject(nullptr, JobObjectLimitViolationInformation_, &j, sizeof j, nullptr); + bound(l.JobMemoryLimit, j.JobMemory); + } + } + } + const kal_u32 n = out->self_size < sizeof v ? out->self_size : static_cast(sizeof v); + auto* d = reinterpret_cast(out); + const auto* src = reinterpret_cast(&v); + for (kal_u32 i = 0; i < n; ++i) d[i] = src[i]; + return kal_ok; +} + } diff --git a/src/observe.cpp b/src/observe.cpp new file mode 100644 index 0000000..2803c3c --- /dev/null +++ b/src/observe.cpp @@ -0,0 +1,708 @@ +// openkal.debug and openkal.prof for a program of this environment (0.23): +// the fault reported by what ends the program, an address named, and samples +// taken (SPEC.md clause 11, entry 56). +// +// THE FAULT is reported from the filter this system calls for an exception no +// handler of the program took --- the one that ends it --- and after the filter +// that was there before, so that a fault a runtime's own filter handles is not +// reported. Memory that may not be there --- a frame of a broken chain --- is +// read through ReadProcessMemory, which refuses where a load would fault again, +// and nothing is allocated: a program that faulted may have done so inside its +// allocator. An exhausted stack is reported from a context of its own. +// +// SAMPLES are taken by a context of this program's own, which suspends each +// other context that used the processor since the last tick, reads where it +// stands, and lets it go: this system delivers no timer to the context that is +// running. Nothing is allocated while a context is suspended; the one that is +// may hold the lock an allocation takes. +#include "win.h" +#include +#include +#include +#include +#include + +namespace okw { bool exec_file_part(okw_uptr address, okw_uptr* view, kal_u64* at); } + +namespace { + +// --- the registers of a context (CONTEXT, of this system's two processors) ------ +#if defined(__aarch64__) || defined(_M_ARM64) +constexpr okw_uptr kContextSize = 0x390, kFlagsAt = 0x0, kFpAt = 0xF0, kSpAt = 0x100, kPcAt = 0x108; +constexpr DWORD kContextWanted = 0x00400003u; // CONTEXT_ARM64 | CONTROL | INTEGER +#else +constexpr okw_uptr kContextSize = 0x4D0, kFlagsAt = 0x30, kFpAt = 0xA0, kSpAt = 0x98, kPcAt = 0xF8; +constexpr DWORD kContextWanted = 0x00100003u; // CONTEXT_AMD64 | CONTROL | INTEGER +#endif + +struct frame_regs { okw_uptr pc, fp, sp; }; + +frame_regs from_context(const void* ctx) { + const auto* b = static_cast(ctx); + frame_regs r{}; + auto word = [&](okw_uptr at) { okw_uptr v = 0; for (int i = 7; i >= 0; --i) v = (v << 8) | b[at + i]; return v; }; + r.pc = word(kPcAt); r.fp = word(kFpAt); r.sp = word(kSpAt); + return r; +} + +bool peek(okw_uptr at, void* out, okw_uptr n) { + unsigned long long got = 0; + return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(at), out, n, &got) && got == n; +} + +// The return addresses of a chain of frame pointers. +int walk(frame_regs r, okw_uptr* pcs, int max) { + int n = 0; + if (r.pc && n < max) pcs[n++] = r.pc; + okw_uptr fp = r.fp; + for (int i = 0; i < 256 && n < max; ++i) { + if (fp == 0 || (fp & 7) != 0 || fp < r.sp) break; + okw_uptr pair[2]; + if (!peek(fp, pair, sizeof pair)) break; + if (pair[1] == 0) break; + pcs[n++] = pair[1]; + if (pair[0] <= fp) break; // a chain runs toward older frames + fp = pair[0]; + } + return n; +} + +// --- writing without a C library --------------------------------------------- +struct line { + char b[512]; okw_uptr n = 0; + line& s(const char* t) { while (*t && n + 1 < sizeof b) b[n++] = *t++; return *this; } + line& s(const char* t, okw_uptr len) { for (okw_uptr i = 0; i < len && n + 1 < sizeof b; ++i) b[n++] = t[i]; return *this; } + line& hex(okw_uptr v) { + char d[18]; int k = 0; + do { d[k++] = "0123456789abcdef"[v & 15]; v >>= 4; } while (v); + s("0x"); + while (k) { if (n + 1 < sizeof b) b[n++] = d[--k]; else --k; } + return *this; + } + line& dec(okw_uptr v) { + char d[24]; int k = 0; + do { d[k++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (k) { if (n + 1 < sizeof b) b[n++] = d[--k]; else --k; } + return *this; + } + void out() { + void* h = GetStdHandle(STD_ERROR_HANDLE); + DWORD wrote = 0; + for (okw_uptr at = 0; h && h != INVALID_HANDLE_VALUE && at < n; at += wrote) + if (!WriteFile(h, b + at, static_cast(n - at), &wrote, nullptr) || wrote == 0) break; + n = 0; + } +}; + +// --- reading a file at an offset ------------------------------------------------ +struct file { + HANDLE h = INVALID_HANDLE_VALUE; + explicit file(const wchar_t* name) { + h = CreateFileW(name, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + } + ~file() { if (h != INVALID_HANDLE_VALUE) CloseHandle(h); } + bool ok() const { return h != INVALID_HANDLE_VALUE; } + bool read(void* out, okw_uptr n, unsigned long long at) const { + okw_uptr got = 0; + while (got < n) { + OVERLAPPED ov{}; + ov.u.Offset = static_cast(at + got); + ov.u.OffsetHigh = static_cast((at + got) >> 32); + DWORD r = 0; + if (!ReadFile(h, static_cast(out) + got, static_cast(n - got), &r, &ov) || r == 0) return false; + got += r; + } + return true; + } +}; + +// --- where an address lies ----------------------------------------------------- +// +// In a part of a region that is a file's (exec.cpp): the file this system +// names for the view, an ELF image. In an image this system loaded --- the +// program, a library --- the module's file, a PE image, and the offset in it +// from the module's own section headers. +enum class format { none, elf, pe }; +struct where_t { + format kind = format::none; + okw_uptr base = 0; // the module's, for a PE image + okw_uptr offset = 0; // in the file + wchar_t path[600]; + okw_uptr plen = 0; + char shown[600]; // the path, for the report + okw_uptr slen = 0; +}; + +bool locate(okw_uptr addr, where_t& w) { + okw_uptr view = 0; kal_u64 at = 0; + if (okw::exec_file_part(addr, &view, &at)) { + // The object manager's name, opened through its root. + static const wchar_t root[] = L"\\\\?\\GLOBALROOT"; + okw_uptr k = 0; + for (; root[k]; ++k) w.path[k] = root[k]; + const DWORD n = K32GetMappedFileNameW(GetCurrentProcess(), reinterpret_cast(addr), w.path + k, + static_cast(sizeof w.path / sizeof w.path[0] - k - 1)); + if (n == 0) return false; + w.plen = k + n; + w.path[w.plen] = 0; + w.kind = format::elf; + w.offset = addr - view + static_cast(at); + w.slen = okw::narrow(w.path + k, n, w.shown, sizeof w.shown, false); + return true; + } + MEMORY_BASIC_INFORMATION_ m{}; + if (VirtualQuery(reinterpret_cast(addr), &m, sizeof m) == 0 || m.Type != MEM_IMAGE_) return false; + const okw_uptr base = reinterpret_cast(m.AllocationBase); + const DWORD n = GetModuleFileNameW(reinterpret_cast(base), w.path, + static_cast(sizeof w.path / sizeof w.path[0] - 1)); + if (n == 0 || n >= sizeof w.path / sizeof w.path[0] - 1) return false; + w.plen = n; + w.path[n] = 0; + // The section holding the address, in the module's headers as loaded. + unsigned lfanew = 0; + unsigned char fh[20]; + if (!peek(base + 0x3c, &lfanew, 4) || !peek(base + lfanew + 4, fh, sizeof fh)) return false; + const unsigned sections = fh[2] | (fh[3] << 8); + const unsigned optional = fh[16] | (fh[17] << 8); + const okw_uptr rva = addr - base; + w.offset = rva; // where no section holds it: the headers, which are the file's first bytes + for (unsigned i = 0; i < sections && i < 96; ++i) { + unsigned char sh[40]; + if (!peek(base + lfanew + 24 + optional + i * 40, sh, sizeof sh)) break; + auto u32 = [&](int at) { return static_cast(sh[at] | (sh[at + 1] << 8) | (sh[at + 2] << 16)) + | (static_cast(sh[at + 3]) << 24); }; + const okw_uptr va = u32(12), vsize = u32(8), raw = u32(20); + if (rva >= va && rva < va + vsize) { w.offset = rva - va + raw; break; } + } + w.kind = format::pe; + w.base = base; + w.slen = okw::narrow(w.path, n, w.shown, sizeof w.shown, false); + return true; +} + +// --- the symbol that covers an offset ------------------------------------------- +struct ehdr64 { unsigned char ident[16]; okw_u32 tv; okw_u32 version; okw_u64 entry, phoff, shoff; + okw_u32 flags; unsigned short ehsize, phentsize, phnum, shentsize, shnum, shstrndx; }; +struct phdr64 { okw_u32 type, flags; okw_u64 offset, vaddr, paddr, filesz, memsz, align; }; +struct shdr64 { okw_u32 name, type; okw_u64 flags, addr, offset, size; okw_u32 link, info; okw_u64 align, entsize; }; +struct sym64 { okw_u32 name; unsigned char info, other; unsigned short shndx; okw_u64 value, size; }; + +bool elf_symbol(const file& f, okw_uptr off, char* name, okw_uptr cap, okw_uptr& len, okw_uptr& delta) { + ehdr64 eh{}; + if (!f.read(&eh, sizeof eh, 0) || eh.ident[0] != 0x7f || eh.ident[1] != 'E' || eh.ident[4] != 2) return false; + okw_uptr vaddr = ~okw_uptr{0}; + for (unsigned i = 0; i < eh.phnum; ++i) { + phdr64 ph{}; + if (!f.read(&ph, sizeof ph, eh.phoff + i * sizeof ph)) return false; + if (ph.type == 1 && off >= ph.offset && off < ph.offset + ph.filesz) { vaddr = off - ph.offset + ph.vaddr; break; } + } + if (vaddr == ~okw_uptr{0}) return false; + shdr64 table{}, strings{}; + bool have = false; + static const unsigned kinds[2] = { 2 /* SHT_SYMTAB */, 11 /* SHT_DYNSYM */ }; + for (unsigned want : kinds) { + for (unsigned i = 0; i < eh.shnum && !have; ++i) { + shdr64 sh{}; + if (!f.read(&sh, sizeof sh, eh.shoff + i * sizeof sh)) return false; + if (sh.type != want) continue; + if (!f.read(&strings, sizeof strings, eh.shoff + sh.link * sizeof sh)) return false; + table = sh; have = true; + } + if (have) break; + } + if (!have || table.entsize != sizeof(sym64)) return false; + okw_u64 best = 0; okw_u32 best_name = 0; bool any = false; + sym64 chunk[64]; + const okw_u64 count = table.size / sizeof(sym64); + for (okw_u64 at = 0; at < count; at += 64) { + const okw_u64 n = count - at < 64 ? count - at : 64; + if (!f.read(chunk, n * sizeof(sym64), table.offset + at * sizeof(sym64))) return false; + for (okw_u64 k = 0; k < n; ++k) { + const sym64& sy = chunk[k]; + if ((sy.info & 15) != 2 /* STT_FUNC */ && (sy.info & 15) != 1 /* STT_OBJECT */) continue; + if (sy.shndx == 0 || sy.value > vaddr) continue; + if (sy.size != 0 && vaddr >= sy.value + sy.size) continue; + if (!any || sy.value > best) { best = sy.value; best_name = sy.name; any = true; } + } + } + if (!any) return false; + okw_uptr n = 0; + char c = 1; + while (n + 1 < cap && f.read(&c, 1, strings.offset + best_name + n) && c) name[n++] = c; + len = n; delta = vaddr - best; + return n != 0; +} + +// The COFF symbol table a PE file keeps where its linker was not told to drop +// it: eighteen bytes a symbol, the value relative to its section, and the +// strings after the table. +bool pe_symbol(const file& f, okw_uptr rva, char* name, okw_uptr cap, okw_uptr& len, okw_uptr& delta) { + unsigned lfanew = 0; + unsigned char fh[20]; + if (!f.read(&lfanew, 4, 0x3c) || !f.read(fh, sizeof fh, lfanew + 4)) return false; + auto u32 = [](const unsigned char* p) { return static_cast(p[0] | (p[1] << 8) | (p[2] << 16)) + | (static_cast(p[3]) << 24); }; + const unsigned sections = fh[2] | (fh[3] << 8); + const okw_u32 table = u32(fh + 8), count = u32(fh + 12); + const unsigned optional = fh[16] | (fh[17] << 8); + if (table == 0 || count == 0) return false; + // The section that holds the address, numbered from one. + unsigned section = 0; + okw_uptr in_section = 0; + for (unsigned i = 0; i < sections && i < 96; ++i) { + unsigned char sh[40]; + if (!f.read(sh, sizeof sh, lfanew + 24 + optional + i * 40)) return false; + const okw_uptr va = u32(sh + 12), vsize = u32(sh + 8); + if (rva >= va && rva < va + vsize) { section = i + 1; in_section = rva - va; break; } + } + if (section == 0) return false; + okw_u32 best = 0; unsigned char best_name[8] = {}; bool any = false; + unsigned char chunk[64 * 18]; + okw_u32 first = 0, held = 0; // the records `chunk' holds + // An auxiliary record follows its symbol and is not one, so the walk steps + // over them, across the chunks it reads. + for (okw_u32 i = 0; i < count;) { + if (i < first || i >= first + held) { + first = i; + held = count - i < 64 ? count - i : 64; + if (!f.read(chunk, held * 18, table + static_cast(i) * 18)) return false; + } + const unsigned char* sy = chunk + (i - first) * 18; + const okw_u32 value = u32(sy + 8); + const unsigned sec = sy[12] | (sy[13] << 8); + const unsigned type = sy[14] | (sy[15] << 8); + const unsigned cls = sy[16]; + i += 1 + sy[17]; + if (sec != section || (cls != 2 && cls != 3) || value > in_section) continue; + if ((type & 0x30) != 0x20 && cls != 2) continue; // a function, or an external + if (!any || value > best) { best = value; for (int k = 0; k < 8; ++k) best_name[k] = sy[k]; any = true; } + } + if (!any) return false; + okw_uptr n = 0; + if (u32(best_name) == 0) { + const unsigned long long strings = table + static_cast(count) * 18; + char c = 1; + while (n + 1 < cap && f.read(&c, 1, strings + u32(best_name + 4) + n) && c) name[n++] = c; + } else { + while (n < 8 && n + 1 < cap && best_name[n]) { name[n] = static_cast(best_name[n]); ++n; } + } + len = n; delta = in_section - best; + return n != 0; +} + +bool symbol_at(const where_t& w, okw_uptr addr, char* name, okw_uptr cap, okw_uptr& len, okw_uptr& delta) { + file f(w.path); + if (!f.ok()) return false; + if (w.kind == format::elf) return elf_symbol(f, w.offset, name, cap, len, delta); + if (w.kind == format::pe) return pe_symbol(f, addr - w.base, name, cap, len, delta); + return false; +} + +// --- the fault report --------------------------------------------------------------- +struct report_t { + where_t w; + char name[256]; + okw_uptr pcs[64]; +}; +report_t g_report; // one fault is reported; it is not reentered +volatile long long g_reporting = 0; + +void describe_frame(line& l, okw_uptr pc) { + where_t& w = g_report.w; + w = where_t{}; + if (!locate(pc, w)) return; + l.s(" ").s(w.shown, w.slen).s("+").hex(w.offset); + okw_uptr nlen = 0, delta = 0; + if (symbol_at(w, pc, g_report.name, sizeof g_report.name, nlen, delta)) + l.s(" ").s(g_report.name, nlen).s("+").hex(delta); +} + +const char* fault_name(DWORD code) { + switch (code) { + case 0xC0000005u: return "an access the program may not make (EXCEPTION_ACCESS_VIOLATION)"; + case 0xC0000006u: return "an access the memory cannot serve (EXCEPTION_IN_PAGE_ERROR)"; + case 0xC000001Du: return "an instruction the program may not execute (EXCEPTION_ILLEGAL_INSTRUCTION)"; + case 0xC0000096u: return "an instruction the program may not execute (EXCEPTION_PRIV_INSTRUCTION)"; + case 0xC00000FDu: return "the stack is exhausted (EXCEPTION_STACK_OVERFLOW)"; + case 0xC0000094u: return "an arithmetic fault (EXCEPTION_INT_DIVIDE_BY_ZERO)"; + case 0xC0000095u: return "an arithmetic fault (EXCEPTION_INT_OVERFLOW)"; + case 0x80000002u: return "an access the memory cannot serve (EXCEPTION_DATATYPE_MISALIGNMENT)"; + default: return nullptr; + } +} + +void report(const EXCEPTION_POINTERS_* e) { + const EXCEPTION_RECORD_* r = e->ExceptionRecord; + line l; + // An access names the address it reached in its second parameter. + const okw_uptr addr = (r->ExceptionCode == 0xC0000005u || r->ExceptionCode == 0xC0000006u) && r->NumberParameters >= 2 + ? static_cast(r->ExceptionInformation[1]) + : reinterpret_cast(r->ExceptionAddress); + l.s("openkal: fault: ").s(fault_name(r->ExceptionCode)).s(" at ").hex(addr).s("\n"); + l.out(); + const int n = walk(from_context(e->ContextRecord), g_report.pcs, 64); + for (int i = 0; i < n; ++i) { + l.s("openkal: #").dec(static_cast(i)).s(" ").hex(g_report.pcs[i]); + describe_frame(l, i == 0 ? g_report.pcs[i] : g_report.pcs[i] - 1); + l.s("\n"); + l.out(); + } +} + +DWORD OKW_API report_elsewhere(LPVOID e) { report(static_cast(e)); return 0; } + +TOP_LEVEL_FILTER_ g_before = nullptr; + +long OKW_API ending(EXCEPTION_POINTERS_* e) { + // A filter that was there first, and handles the fault, has the program go on. + const long earlier = g_before ? g_before(e) : 0 /* EXCEPTION_CONTINUE_SEARCH */; + if (earlier == -1 /* EXCEPTION_CONTINUE_EXECUTION */) return earlier; + if (fault_name(e->ExceptionRecord->ExceptionCode) && okw::atomic::exchange(&g_reporting, 1) == 0) { + if (e->ExceptionRecord->ExceptionCode == 0xC00000FDu) { + // An exhausted stack has no room for the report. + HANDLE t = CreateThread(nullptr, 256 * 1024, &report_elsewhere, e, 0, nullptr); + if (t) { WaitForSingleObject(t, 10000); CloseHandle(t); } + } else { + report(e); + } + } + return earlier; +} + +// INSTALLED AT INCEPTION, for every program of this implementation. +OKW_START_FN(install_fault_report) { g_before = SetUnhandledExceptionFilter(&ending); } +OKW_AT_START(install_fault_report); + +// --- touches (0.23) ------------------------------------------------------------------ +// +// A part being recorded keeps its access and loses its execution; this +// system then reports a first execution of a page as an access violation of +// the kind that executes (the first parameter 8), which a first look resolves +// --- the page executable again, its address recorded once --- and the +// instruction is carried out. Every other exception passes on to the +// program's handlers. +struct touched_part { + okw_uptr from = 0, to = 0; + DWORD prot = 0; + volatile long long* seen = nullptr; // a bit for each page +}; +constexpr int kTouchedParts = 16; +touched_part g_parts[kTouchedParts]; +volatile long long g_part_count = 0; +volatile long long g_touching = 0; +okw_uptr g_touch_page = 4096; +void* g_touch_handler = nullptr; + +struct touch_ring { + okw_uptr* pages = nullptr; + volatile long long* ready = nullptr; + okw_uptr cap = 0; + volatile long long written = 0, taken = 0; +}; +touch_ring g_touches; + +DWORD without_execution(DWORD prot) { + switch (prot) { + case 0x10: return PAGE_NOACCESS; // PAGE_EXECUTE + case 0x20: return PAGE_READONLY; // PAGE_EXECUTE_READ + case 0x40: return PAGE_READWRITE; // PAGE_EXECUTE_READWRITE + case 0x80: return PAGE_WRITECOPY; // PAGE_EXECUTE_WRITECOPY + default: return 0; + } +} + +// One protection for the whole of [from, to), and that one executable; 0 otherwise. +DWORD protection_of(okw_uptr from, okw_uptr to) { + DWORD prot = 0; + for (okw_uptr at = from; at < to;) { + MEMORY_BASIC_INFORMATION_ m{}; + if (VirtualQuery(reinterpret_cast(at), &m, sizeof m) == 0 || m.State != MEM_COMMIT) return 0; + if (prot == 0) prot = m.Protect; else if (prot != m.Protect) return 0; + at = reinterpret_cast(m.BaseAddress) + m.RegionSize; + } + return without_execution(prot) ? prot : 0; +} + +// The protection of [from, to) set, each run of it within one allocation in +// turn: a call of this system's own reaches no further than one view. +void protect_runs(okw_uptr from, okw_uptr to, DWORD prot) { + for (okw_uptr at = from; at < to;) { + MEMORY_BASIC_INFORMATION_ m{}; + if (VirtualQuery(reinterpret_cast(at), &m, sizeof m) == 0) return; + okw_uptr end = reinterpret_cast(m.BaseAddress) + m.RegionSize; + if (end > to) end = to; + DWORD before = 0; + VirtualProtect(reinterpret_cast(at), end - at, prot, &before); + at = end; + } +} + +long OKW_API touch_look(EXCEPTION_POINTERS_* e) { + const EXCEPTION_RECORD_* r = e->ExceptionRecord; + if (r->ExceptionCode != 0xC0000005u || r->NumberParameters < 2 || r->ExceptionInformation[0] != 8 + || !okw::atomic::load(&g_touching)) + return 0; // EXCEPTION_CONTINUE_SEARCH + const okw_uptr addr = static_cast(r->ExceptionInformation[1]); + const long long n = okw::atomic::load(&g_part_count); + for (long long k = 0; k < n; ++k) { + touched_part& t = g_parts[k]; + if (addr < t.from || addr >= t.to) continue; + const okw_uptr page = addr & ~(g_touch_page - 1); + const okw_uptr index = (page - t.from) / g_touch_page; + const long long bit = 1ll << (index & 63); + const long long was = okw::atomic::fetch_or(&t.seen[index >> 6], bit); + DWORD before = 0; + VirtualProtect(reinterpret_cast(page), g_touch_page, t.prot, &before); + if (!(was & bit) && g_touches.pages) { + const long long mine = okw::atomic::fetch_add(&g_touches.written, 1); + if (static_cast(mine - okw::atomic::load(&g_touches.taken)) < g_touches.cap) { + g_touches.pages[static_cast(mine) % g_touches.cap] = page; + okw::atomic::store(&g_touches.ready[static_cast(mine) % g_touches.cap], mine + 1); + } + } + return -1; // EXCEPTION_CONTINUE_EXECUTION + } + return 0; +} + +// --- samples ----------------------------------------------------------------------- +constexpr okw_uptr kRing = 65536; +constexpr okw_uptr kCommitChunk = 64; // samples committed at a time + +struct ring { + kal_prof_sample* slots = nullptr; + volatile long long* ready = nullptr; // the sequence number + 1 of the sample a slot holds + volatile long long written = 0; + volatile long long taken = 0; + volatile long long on = 0; + volatile long long stop = 0; + HANDLE thread = nullptr; + DWORD sampler = 0; + DWORD period_us = 0; + bool committed[kRing / kCommitChunk]; +}; +ring g_ring; + +struct watched { DWORD id; HANDLE h; unsigned long long cycles; }; +constexpr int kWatched = 256; + +DWORD OKW_API sampler(LPVOID) { + static watched threads[kWatched]; + int count = 0; + const DWORD self = GetCurrentThreadId(); + const DWORD pid = GetCurrentProcessId(); + HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION_, TIMER_ALL_ACCESS_); + if (!timer) timer = CreateWaitableTimerExW(nullptr, nullptr, 0, TIMER_ALL_ACCESS_); + LARGE_INTEGER due{}; + due.QuadPart = -static_cast(g_ring.period_us) * 10; + const LONG period_ms = static_cast(g_ring.period_us / 1000 ? g_ring.period_us / 1000 : 1); + if (timer) SetWaitableTimer(timer, &due, period_ms, nullptr, nullptr, FALSE); + alignas(16) static unsigned char context[kContextSize]; + unsigned ticks = 0; + while (!okw::atomic::load(&g_ring.stop)) { + if (timer) WaitForSingleObject(timer, INFINITE); else Sleep(period_ms); + // The contexts of this program, looked for again every tenth of a second. + if (ticks++ % (100000 / (g_ring.period_us ? g_ring.period_us : 1000) + 1) == 0) { + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD_, 0); + if (snap != INVALID_HANDLE_VALUE) { + THREADENTRY32_ te{}; + te.dwSize = sizeof te; + for (BOOL more = Thread32First(snap, &te); more; more = Thread32Next(snap, &te)) { + if (te.th32OwnerProcessID != pid || te.th32ThreadID == self) continue; + bool known = false; + for (int i = 0; i < count && !known; ++i) known = threads[i].id == te.th32ThreadID; + if (known || count == kWatched) continue; + HANDLE h = OpenThread(THREAD_SUSPEND_RESUME_ | THREAD_GET_CONTEXT_ | THREAD_QUERY_LIMITED_INFORMATION_, + FALSE, te.th32ThreadID); + if (h) threads[count++] = watched{ te.th32ThreadID, h, 0 }; + } + CloseHandle(snap); + } + } + for (int i = 0; i < count; ++i) { + watched& t = threads[i]; + // A context that ended is forgotten; one that did not run is not sampled. + if (WaitForSingleObject(t.h, 0) == WAIT_OBJECT_0) { + CloseHandle(t.h); + threads[i--] = threads[--count]; + continue; + } + // What it has used: the processor's cycles where this system counts + // them for a context, its times otherwise --- coarser, and a context + // that ran less than a tick of them is not seen to have run. + unsigned long long used = 0; + bool known = QueryThreadCycleTime(t.h, &used) != 0; + if (!known) { + FILETIME created{}, exited{}, kernel{}, user{}; + known = GetThreadTimes(t.h, &created, &exited, &kernel, &user) != 0; + used = ((static_cast(kernel.dwHighDateTime) << 32) | kernel.dwLowDateTime) + + ((static_cast(user.dwHighDateTime) << 32) | user.dwLowDateTime); + } + if (known && used == t.cycles) continue; + t.cycles = used; + const long long at = okw::atomic::load(&g_ring.written); + if (static_cast(at - okw::atomic::load(&g_ring.taken)) >= kRing) continue; // full: not recorded + const okw_uptr slot = static_cast(at) % kRing; + if (!g_ring.committed[slot / kCommitChunk]) { + if (!VirtualAlloc(&g_ring.slots[slot - slot % kCommitChunk], kCommitChunk * sizeof(kal_prof_sample), + MEM_COMMIT, PAGE_READWRITE)) + continue; + g_ring.committed[slot / kCommitChunk] = true; + } + kal_prof_sample& s = g_ring.slots[slot]; + for (okw_uptr k = 0; k < kContextSize; ++k) context[k] = 0; + *reinterpret_cast(context + kFlagsAt) = kContextWanted; + if (SuspendThread(t.h) == static_cast(-1)) continue; + const bool have = GetThreadContext(t.h, context) != 0; + int n = 0; + if (have) { + okw_uptr pcs[KAL_PROF_SAMPLE_DEPTH]; + n = walk(from_context(context), pcs, KAL_PROF_SAMPLE_DEPTH); + for (int k = 0; k < n; ++k) s.pc[k] = pcs[k]; + } + ResumeThread(t.h); + if (!have) continue; + s.time_ns = kal_time_monotonic(); + s.context = t.id; + s.depth = static_cast(n); + s.reserved = 0; + g_ring.written = at + 1; + okw::atomic::store(&g_ring.ready[slot], at + 1); + } + } + for (int i = 0; i < count; ++i) CloseHandle(threads[i].h); + if (timer) CloseHandle(timer); + return 0; +} + +} // namespace + +extern "C" { + +kal_uintptr kal_debug_props(void) { return KAL_DEBUG_PROP_FAULT_REPORT; } + +// AN ADDRESS NAMED (0.23 for a program of this environment): the file that +// holds it --- an image a loader made a region's part, the program, a library +// --- and that file's symbol table. +kal_intptr kal_debug_symbolize(kal_uintptr address, char* out, kal_uintptr cap, kal_uintptr* start) { + if (address == 0) return -static_cast(kal_err_not_found); + where_t w; + if (!locate(address, w)) return -static_cast(kal_err_not_found); + char name[1024]; + okw_uptr nlen = 0, delta = 0; + if (!symbol_at(w, address, name, sizeof name, nlen, delta)) return -static_cast(kal_err_not_found); + if (out && cap) for (okw_uptr i = 0; i < nlen && i < cap; ++i) out[i] = name[i]; + if (start) *start = address - delta; + return static_cast(nlen); +} + +// THIS IMPLEMENTATION DOES NOT MEASURE A PROGRAM ITSELF: a loader that binds +// an image does (clause 12.3). So the program is not measured, a counter is +// accepted and never read, and a phase is accepted and recorded nowhere. +int kal_prof_active(void) { return 0; } +int kal_prof_counter(const char*, kal_uintptr, const volatile kal_u64*) { return kal_ok; } +void kal_prof_mark(const char*, kal_uintptr, int) {} + +kal_uintptr kal_prof_props(void) { return KAL_PROF_PROP_SAMPLES | KAL_PROF_PROP_TOUCHES; } + +int kal_prof_sample_begin(kal_u32 per_second) { + if (per_second == 0 || per_second > 10000) return kal_err_invalid; + if (okw::atomic::exchange(&g_ring.on, 1) != 0) return kal_err_again; + if (!g_ring.slots) { + // Addresses for a minute of samples at a thousand a second, committed + // as samples are written; the marks beside them, committed now. + g_ring.slots = static_cast(VirtualAlloc(nullptr, kRing * sizeof(kal_prof_sample), + MEM_RESERVE, PAGE_READWRITE)); + g_ring.ready = static_cast(VirtualAlloc(nullptr, kRing * sizeof(long long), + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (!g_ring.slots || !g_ring.ready) { okw::atomic::store(&g_ring.on, 0); return kal_err_no_memory; } + } + g_ring.period_us = 1000000u / per_second; + okw::atomic::store(&g_ring.stop, 0); + g_ring.thread = CreateThread(nullptr, 64 * 1024, &sampler, nullptr, 0, &g_ring.sampler); + if (!g_ring.thread) { okw::atomic::store(&g_ring.on, 0); return kal_err_no_memory; } + return kal_ok; +} + +kal_intptr kal_prof_sample_take(kal_prof_sample* out, kal_uintptr count) { + if (out == nullptr && count != 0) return -static_cast(kal_err_invalid); + if (!g_ring.slots) return 0; + kal_uintptr n = 0; + while (n < count) { + const long long t = okw::atomic::load(&g_ring.taken); + if (okw::atomic::load(&g_ring.ready[static_cast(t) % kRing]) != t + 1) break; + out[n++] = g_ring.slots[static_cast(t) % kRing]; + okw::atomic::store(&g_ring.taken, t + 1); + } + return static_cast(n); +} + +int kal_prof_sample_end(void) { + if (!okw::atomic::load(&g_ring.on)) return kal_err_invalid; + okw::atomic::store(&g_ring.stop, 1); + if (g_ring.thread) { + WaitForSingleObject(g_ring.thread, INFINITE); + CloseHandle(g_ring.thread); + g_ring.thread = nullptr; + } + okw::atomic::store(&g_ring.on, 0); + return kal_ok; +} + +// TOUCHES (0.23). +int kal_prof_touch_begin(const void* at, kal_uintptr size) { + const okw_uptr g = kal_exec_granularity(); + const okw_uptr from = reinterpret_cast(at); + if (size == 0 || (from & (g - 1)) != 0 || (size & (g - 1)) != 0) return kal_err_invalid; + const DWORD prot = protection_of(from, from + size); + if (prot == 0) return kal_err_invalid; + const long long k = okw::atomic::load(&g_part_count); + if (k == kTouchedParts) return kal_err_no_memory; + g_touch_page = g; + if (!g_touches.pages) { + constexpr okw_uptr cap = 65536; + auto* m = static_cast(VirtualAlloc(nullptr, cap * (sizeof(okw_uptr) + sizeof(long long)), + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (!m) return kal_err_no_memory; + g_touches.pages = reinterpret_cast(m); + g_touches.ready = reinterpret_cast(m + cap * sizeof(okw_uptr)); + g_touches.cap = cap; + } + const okw_uptr words = (size / g + 63) / 64; + auto* seen = static_cast(VirtualAlloc(nullptr, words * sizeof(long long), + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (!seen) return kal_err_no_memory; + if (!g_touch_handler) g_touch_handler = AddVectoredExceptionHandler(1, &touch_look); + if (!g_touch_handler) return kal_err_no_memory; + g_parts[k] = touched_part{ from, from + size, prot, seen }; + okw::atomic::store(&g_part_count, k + 1); + okw::atomic::store(&g_touching, 1); + protect_runs(from, from + size, without_execution(prot)); + return kal_ok; +} + +kal_intptr kal_prof_touch_take(kal_uintptr* out, kal_uintptr count) { + if (out == nullptr && count != 0) return -static_cast(kal_err_invalid); + if (!g_touches.pages) return 0; + kal_uintptr n = 0; + while (n < count) { + const long long t = okw::atomic::load(&g_touches.taken); + if (okw::atomic::load(&g_touches.ready[static_cast(t) % g_touches.cap]) != t + 1) break; + out[n++] = g_touches.pages[static_cast(t) % g_touches.cap]; + okw::atomic::store(&g_touches.taken, t + 1); + } + return static_cast(n); +} + +// Every part as it was; the pages' bits stay for the life of the program. +int kal_prof_touch_end(void) { + const long long n = okw::atomic::load(&g_part_count); + if (n == 0) return kal_err_invalid; + for (long long k = 0; k < n; ++k) protect_runs(g_parts[k].from, g_parts[k].to, g_parts[k].prot); + okw::atomic::store(&g_touching, 0); + okw::atomic::store(&g_part_count, 0); + return kal_ok; +} + +} // extern "C" diff --git a/src/process.cpp b/src/process.cpp index e7f29ef..33b9834 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1,5 +1,7 @@ #include "win.h" #include "handle.h" +#include "rights.h" +#include #include #include @@ -73,6 +75,9 @@ okw_uptr plain_name(wchar_t* s, okw_uptr n) { // call. SRWLOCK_ g_starting{}; +// The granted directories and the records of recorded ones (0.23). +constexpr kal_uintptr kMaxGrants = 16; + struct inheritance { HANDLE handle[3]{}; DWORD before[3]{}; @@ -81,10 +86,14 @@ struct inheritance { // 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]{}; + // otherwise be. After the streams, the duplicates a start makes for its + // grants and their records, which are made inheritable when they are made + // and released after the start (`granting'). + HANDLE list[3 + 2 * kMaxGrants]{}; DWORD count = 0; + void include(HANDLE h) { if (count < sizeof list / sizeof list[0]) list[count++] = h; } + inheritance(bool active, HANDLE in, HANDLE out, HANDLE err) { if (!active) return; AcquireSRWLockExclusive(&g_starting); @@ -369,20 +378,208 @@ bool entries_well_formed(const char** envp, const kal_uintptr* lens, kal_uintptr return true; } +// --- what a start conveys beside its arguments (0.23) ------------------------- +// +// THE GRANTS. This system has no numbering a preopen could arrive under, and +// needs none: a handle a program inherits has the value it had in the program +// that started it. So each grant is a duplicate of the caller's handle, made +// inheritable for this start and named in its explicit list, and the started +// program learns the values and the names from one variable, +// +// KAL_PREOPENS={;,,} +// +// and what each carries (rights.h) from a second, +// +// KAL_GRANT_RIGHTS={;,,,,} +// +// where `' is this program: the started program reads them only when the +// program that started it is that one (fs.cpp), so a value that reaches a +// program through one that did not read it --- a shell --- names no one. The +// arrangement is the other two systems', with the starter's number in place of +// the started program's, which this system gives only once the program exists. +// THE PREFERENCE travels as KAL_RESOURCES, read by env.cpp. +bool named(const char* s, okw_uptr n, const char* key) { + okw_uptr k = 0; + for (; key[k]; ++k) { + if (k >= n) return false; + char a = s[k]; + if (a >= 'a' && a <= 'z') a = static_cast(a - 32); + if (a != key[k]) return false; + } + return k < n && s[k] == '='; +} +bool named(const wchar_t* s, okw_uptr n, const char* key) { + okw_uptr k = 0; + for (; key[k]; ++k) { + if (k >= n) return false; + wchar_t a = s[k]; + if (a >= L'a' && a <= L'z') a = static_cast(a - 32); + if (a != static_cast(key[k])) return false; + } + return k < n && s[k] == L'='; +} +template +bool conveyed_here(const C* s, okw_uptr n) { + return named(s, n, "KAL_PREOPENS") || named(s, n, "KAL_GRANT_RIGHTS") || named(s, n, "KAL_RESOURCES"); +} + +char* put_decimal(char* o, unsigned long long v) { + char b[24]; int i = 0; + do { b[i++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (i) *o++ = b[--i]; + return o; +} + +struct granting { + HANDLE dup[kMaxGrants]{}; + HANDLE rec[kMaxGrants]{}; + kal_uintptr count = 0; + const kal_preopen* grants = nullptr; + char rights[8192]; + okw_uptr rights_len = 0; + + int make(const kal_preopen* g, kal_uintptr n, inheritance& window) { + grants = g; + const HANDLE self = GetCurrentProcess(); + for (kal_uintptr i = 0; i < n; ++i) { + void* src = okw::unpack(g[i].dir.h); + if (!DuplicateHandle(self, src, self, &dup[i], 0, TRUE, DUPLICATE_SAME_ACCESS)) + return okw::translate_win32(GetLastError()); + count = i + 1; + window.include(dup[i]); + okw_u32 flags = 0; void* record = nullptr; const char* prefix = ""; okw_uptr plen = 0; + if (!okw::rights::conveyed(src, &flags, &record, &prefix, &plen)) continue; + if (record != nullptr) { + if (!DuplicateHandle(self, record, self, &rec[i], 0, TRUE, DUPLICATE_SAME_ACCESS)) + return okw::translate_win32(GetLastError()); + window.include(rec[i]); + } + char head[96]; + char* o = head; + *o++ = ';'; o = put_decimal(o, i); + *o++ = ','; o = put_decimal(o, flags); + *o++ = ','; + if (rec[i] == nullptr) { *o++ = '-'; *o++ = '1'; } + else o = put_decimal(o, reinterpret_cast(rec[i])); + *o++ = ','; o = put_decimal(o, plen); + *o++ = ','; + const okw_uptr hn = static_cast(o - head); + if (rights_len + hn + plen >= sizeof rights) return kal_err_invalid; + for (okw_uptr k = 0; k < hn; ++k) rights[rights_len++] = head[k]; + for (okw_uptr k = 0; k < plen; ++k) rights[rights_len++] = prefix[k]; + } + return kal_ok; + } + ~granting() { + for (kal_uintptr i = 0; i < count; ++i) { + if (dup[i]) CloseHandle(dup[i]); + if (rec[i]) CloseHandle(rec[i]); + } + } +}; + +// The block of named values a start passes, made of the caller's entries --- +// or this program's own where the caller gave none --- less any of the names +// a start conveys, and this start's. +struct environment { + wchar_t* w = nullptr; + okw_uptr cap = 0, at = 0; + char* extra = nullptr; + okw_uptr extra_cap = 0, extra_len = 0; + + ~environment() { + if (w) kal_free(w, cap * sizeof(wchar_t), alignof(wchar_t)); + if (extra) kal_free(extra, extra_cap, 1); + } + + bool put_utf8(const char* s, okw_uptr n) { + if (n == 0) return true; + const int produced = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, static_cast(n), + w + at, static_cast(cap - at - 2)); + if (produced <= 0) return false; + at += static_cast(produced); + w[at++] = 0; + return true; + } + + int build(const char** envp, const kal_uintptr* lens, kal_uintptr envc, const granting* g, + const char* resources) { + // This start's own, first, as text of the caller's encoding. + okw_uptr need = 64; + if (g) { + need += 32; + for (kal_uintptr i = 0; i < g->count; ++i) need += 48 + g->grants[i].len; + need += 48 + g->rights_len; + } + extra_cap = need; + extra = static_cast(kal_alloc(extra_cap, 1)); + if (!extra) return kal_err_no_memory; + const unsigned long long pid = GetCurrentProcessId(); + char* o = extra; + auto text = [&](const char* t) { while (*t) *o++ = *t++; }; + if (g) { + text("KAL_PREOPENS="); o = put_decimal(o, pid); + for (kal_uintptr i = 0; i < g->count; ++i) { + *o++ = ';'; o = put_decimal(o, reinterpret_cast(g->dup[i])); + *o++ = ','; o = put_decimal(o, g->grants[i].len); + *o++ = ','; + for (kal_uintptr k = 0; k < g->grants[i].len; ++k) *o++ = g->grants[i].name[k]; + } + *o++ = '\0'; + if (g->rights_len) { + text("KAL_GRANT_RIGHTS="); o = put_decimal(o, pid); + for (okw_uptr k = 0; k < g->rights_len; ++k) *o++ = g->rights[k]; + *o++ = '\0'; + } + } + if (resources) { text("KAL_RESOURCES="); text(resources); *o++ = '\0'; } + extra_len = static_cast(o - extra); + + // The rest, measured: no unit of this system's encoding is made of + // fewer than one byte of the caller's. + wchar_t* own = envc == 0 ? GetEnvironmentStringsW() : nullptr; + okw_uptr total = extra_len + 2; + if (envc > 0) for (kal_uintptr i = 0; i < envc; ++i) total += lens[i] + 1; + else if (own) { const wchar_t* p = own; while (*p) { while (*p) { ++p; ++total; } ++p; ++total; } } + cap = total; + w = static_cast(kal_alloc(cap * sizeof(wchar_t), alignof(wchar_t))); + if (!w) { if (own) FreeEnvironmentStringsW(own); return kal_err_no_memory; } + bool ok = true; + if (envc > 0) { + for (kal_uintptr i = 0; i < envc && ok; ++i) + if (!conveyed_here(envp[i], lens[i])) ok = put_utf8(envp[i], lens[i]); + } else if (own) { + for (const wchar_t* p = own; *p;) { + okw_uptr n = 0; + while (p[n]) ++n; + if (!conveyed_here(p, n)) { for (okw_uptr k = 0; k <= n; ++k) w[at++] = p[k]; } + p += n + 1; + } + FreeEnvironmentStringsW(own); + } + for (const char* e = extra; ok && e < extra + extra_len;) { + const okw_uptr n = okw::length(e); + ok = put_utf8(e, n); + e += n + 1; + } + if (!ok) return kal_err_invalid; + // An empty block is one terminator before the last. + if (at == 0) w[at++] = 0; + w[at++] = 0; + return kal_ok; + } +}; + } // namespace extern "C" { // Starting a program. One function since openkal 0.11. // -// 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 -// there is no correspondence to descriptor three. KAL_PROCESS_PROP_GRANT_DIR is -// not claimed. A count of zero asks for a program with no preopens, which is -// what a program here gets anyway, so that request IS answerable and is -// answered. +// `grants' WAS REFUSED HERE UNTIL 0.19.0 (openkal 0.23), on the ground that this +// environment has no numbering a preopen could arrive under. It needs none: an +// inherited handle keeps its value, and the value travels with the name +// (`granting' above). A count of zero starts a program with no preopens. // // `KAL_SPAWN_BOUND_LIFETIME' WAS REFUSED HERE UNTIL 0.13, on the ground that no // primitive arms it from inside the started image. None needs to: a job that @@ -427,9 +624,26 @@ int kal_process_spawn(const kal_spawn* how, void* run = okw::unpack(how->work.h); if (!dir || !run) return kal_err_invalid; if (!okw::acceptable(path, path_len)) return kal_err_invalid; - if (how->grant_count > 0) return kal_err_not_supported; constexpr kal_uintptr nulls = KAL_SPAWN_NULL_IN | KAL_SPAWN_NULL_OUT | KAL_SPAWN_NULL_ERR; - if ((how->flags & ~(KAL_SPAWN_BOUND_LIFETIME | KAL_SPAWN_ONE_STRING | nulls)) != 0) return kal_err_not_supported; + constexpr kal_uintptr preferences = KAL_SPAWN_RESOURCES_BALANCED | KAL_SPAWN_RESOURCES_PERFORMANCE + | KAL_SPAWN_RESOURCES_ECONOMY; + // KAL_SPAWN_CONFINE is not among them: this system holds a program to + // directories only by changing who may reach them for everyone (a + // security descriptor of each file), which a start does not get to do. + if ((how->flags & ~(KAL_SPAWN_BOUND_LIFETIME | KAL_SPAWN_ONE_STRING | nulls | preferences)) != 0) + return kal_err_not_supported; + const kal_uintptr preference = how->flags & preferences; + if (preference & (preference - 1)) return kal_err_invalid; + // THE GRANTS (0.23): each a directory this program holds, and a name that + // travels in a variable, which cannot carry a zero. + if (how->grants == nullptr && how->grant_count != 0) return kal_err_invalid; + if (how->grant_count > kMaxGrants) return kal_err_invalid; + for (kal_uintptr i = 0; i < how->grant_count; ++i) { + const kal_preopen& g = how->grants[i]; + if (!okw::unpack(g.dir.h)) return kal_err_invalid; + if (g.len >= okw::kMaxName || (g.len > 0 && g.name == nullptr)) return kal_err_invalid; + for (kal_uintptr c = 0; c < g.len; ++c) if (g.name[c] == '\0') return kal_err_invalid; + } // THE NULL DEVICE BESIDE A STREAM AT ONE POSITION is two answers to one // question (openkal 0.22, clause 7.15). if (streams != nullptr @@ -477,7 +691,6 @@ int kal_process_spawn(const kal_spawn* how, // beyond what `line' holds had the quoting overwrite what it had not // yet read. wchar_t one[kCommandLine]; - wchar_t block[kCommandLine]; }; auto* work = static_cast(kal_alloc(sizeof(scratch), alignof(scratch))); if (!work) return kal_err_no_memory; @@ -533,16 +746,20 @@ int kal_process_spawn(const kal_spawn* how, line[used] = 0; if (argc == 0) line[0] = 0; - // The named values. An empty set means the started program receives the - // caller's, which is what this environment does when none is supplied. - wchar_t* block = work->block; - okw_uptr block_used = 0; - for (kal_uintptr i = 0; i < envc; ++i) { - if (!append_utf8(block, kCommandLine, block_used, envp[i], envp_lens[i])) - return kal_err_no_space; - block[block_used++] = 0; - } - block[block_used++] = 0; + // THE NAMED VALUES. An empty set means the started program receives the + // caller's, which is what this environment does when none is supplied --- + // unless this start conveys something of its own (0.23: the grants, what + // they carry, the preference), and then the block is the caller's, less + // any value of those names, and this start's. Made inside the start's + // window below, because the grants' handles are made there. + const int own_preference = kal_env_resources(); + const int asked_preference = preference == KAL_SPAWN_RESOURCES_PERFORMANCE ? KAL_RESOURCES_PERFORMANCE + : preference == KAL_SPAWN_RESOURCES_ECONOMY ? KAL_RESOURCES_ECONOMY + : preference == KAL_SPAWN_RESOURCES_BALANCED ? KAL_RESOURCES_BALANCED + : own_preference; + const char* resources = asked_preference == KAL_RESOURCES_PERFORMANCE ? "performance" + : asked_preference == KAL_RESOURCES_ECONOMY ? "economy" + : preference != 0 ? "balanced" : nullptr; // THE NULL DEVICE (openkal 0.22): this system's `NUL', opened for this // start and released after it; the started program holds its own handle. @@ -563,7 +780,10 @@ int kal_process_spawn(const kal_spawn* how, STARTUPINFOW& startup = ex.startup; startup.cb = sizeof(STARTUPINFOW); bool inherit = false; - if (null_at != 0 || (streams && (streams->in.h || streams->out.h || streams->err.h))) { + const bool grants = how->grants != nullptr; + // A start that grants places the streams as one that is given them does: + // what it places is named in the list, and nothing else crosses. + if (grants || null_at != 0 || (streams && (streams->in.h || streams->out.h || streams->err.h))) { auto placed = [&](kal_uintptr flag, kal_uintptr stream, DWORD standard) -> HANDLE { if (null_at & flag) return device.h; return stream ? reinterpret_cast(stream) : GetStdHandle(standard); @@ -578,9 +798,22 @@ int kal_process_spawn(const kal_spawn* how, PROCESS_INFORMATION info{}; BOOL started = FALSE; DWORD refusal = 0; + int failed = kal_ok; { inheritance window(inherit, startup.hStdInput, startup.hStdOutput, startup.hStdError); + // THE GRANTS AND THEIR RECORDS, as duplicates made inheritable for this + // start: the started program receives each at the value it has here, + // and its name and what it carries in the variables below. + granting given; + environment block; + if (grants || resources != nullptr) { + failed = given.make(how->grants, how->grant_count, window); + if (failed == kal_ok) failed = block.build(envp, envp_lens, envc, grants ? &given : nullptr, resources); + } else if (envc > 0) { + failed = block.build(envp, envp_lens, envc, nullptr, nullptr); + } + // 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 @@ -598,14 +831,24 @@ int kal_process_spawn(const kal_spawn* how, inherit_handles = TRUE; } - started = CreateProcessW(image, argc ? line : nullptr, nullptr, nullptr, - 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(); + if (failed == kal_ok) { + started = CreateProcessW(image, argc ? line : nullptr, nullptr, nullptr, + inherit_handles, creation, + block.w, cwd, &startup, &info); + // Read before the handles are put back, which may set it again. + if (!started) refusal = GetLastError(); + } } + if (failed != kal_ok) return failed; if (!started) return okw::translate_win32(refusal); + // ECONOMY (0.23): the started program's pages are the first this system + // takes back when memory is short. Set by the starter, so that a program + // of this system's own --- one that does not read the preference --- is + // held to it as well; a program of this implementation also sets it on + // itself where it was started otherwise (env.cpp). + if (asked_preference == KAL_RESOURCES_ECONOMY) okw::lower_memory_priority(info.hProcess); + // 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. @@ -695,17 +938,27 @@ int kal_process_info(kal_process p, kal_u32 wanted, kal_program_info* out) { } // MEMORY (openkal 0.20): the commit charge now and at most; the process // object keeps both after the program ends, while the handle is held. + // PAGE FAULTS (0.23): this system counts them as one number, which is + // `faults_minor' with KAL_PINFO_MAJOR absent. Each is written where the + // caller's structure reaches it. + constexpr kal_u32 memory_end = 56, faults_end = 72; PROCESS_MEMORY_COUNTERS_EX_ m{}; m.cb = sizeof m; + const bool counted = (wanted & (KAL_PINFO_MEMORY | KAL_PINFO_FAULTS)) && self >= memory_end + && K32GetProcessMemoryInfo(kind_of(p) == kind_self ? GetCurrentProcess() : h, &m, sizeof m); // An ended program whose counters read zero has no record of them left // (wine keeps none), which is not a peak of zero: nothing is reported. - if ((wanted & KAL_PINFO_MEMORY) && self >= sizeof v - && K32GetProcessMemoryInfo(kind_of(p) == kind_self ? GetCurrentProcess() : h, &m, sizeof m) - && (!v.ended || m.PeakPagefileUsage != 0)) { + if (counted && (wanted & KAL_PINFO_MEMORY) && (!v.ended || m.PeakPagefileUsage != 0)) { v.memory_bytes = v.ended ? 0 : m.PagefileUsage; v.memory_peak_bytes = m.PeakPagefileUsage > m.PagefileUsage ? m.PeakPagefileUsage : m.PagefileUsage; v.present |= KAL_PINFO_MEMORY; } + // A count of zero is no count: every program has taken a fault by the time + // it can ask, and an emulator of this system that keeps none answers zero. + if (counted && (wanted & KAL_PINFO_FAULTS) && self >= faults_end && m.PageFaultCount != 0) { + v.faults_minor = m.PageFaultCount; + v.present |= KAL_PINFO_FAULTS; + } const kal_u32 n = self < sizeof v ? self : static_cast(sizeof v); auto* d = reinterpret_cast(out); const auto* s = reinterpret_cast(&v); @@ -936,6 +1189,21 @@ void kal_process_channel_close(kal_stream s) { } +// THE NUMBERS THIS SYSTEM GIVES A PROGRAM IT ENDED (0.23): the status of a +// fault no handler took --- an access, an instruction, an exhausted stack, a +// failure the program asked to be ended for --- which is an error of the +// system's own facility (0xC000xxxx), or the two warnings a fault can end a +// program with; the number this implementation gives one it terminates; and +// the console's interrupt. Until 0.19.0 only the fail-fast status was among +// the first, and a program that faulted was reported to have returned a +// number. +namespace { +bool ended_by_the_system(DWORD code) { + return (code & 0xFFFF0000u) == 0xC0000000u || code == 0x80000002u /* misalignment */ + || code == 0x80000003u /* a breakpoint */ || code == 0xFFFFFFFFu || code == STATUS_CONTROL_C_EXIT_; +} +} // namespace + int kal_process_wait(kal_process p, int* status, int* terminated) { if (kind_of(p) == kind_self) return kal_err_invalid; if (kind_of(p) == kind_opened) return kal_err_not_supported; @@ -957,7 +1225,7 @@ int kal_process_wait(kal_process p, int* status, int* terminated) { if (code == kLimitCode || ended_by_limit(h)) { if (status) *status = static_cast(code & 0x7fffffff); if (terminated) *terminated = KAL_TERMINATED_LIMIT; - } else if (code == 0xC0000409u || code == 0xFFFFFFFFu || code == STATUS_CONTROL_C_EXIT_) { + } else if (ended_by_the_system(code)) { if (status) *status = static_cast(code & 0x7fffffff); if (terminated) *terminated = 1; } else { @@ -1007,9 +1275,10 @@ void kal_process_close(kal_process p) { // 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: // -// GRANT_DIR kal_process_spawn refuses a non-empty `grants' +// CONFINE kal_process_spawn refuses KAL_SPAWN_CONFINE (0.23) // -// BOUND_LIFETIME and STOP_REQUESTED were absent until 0.13 (K5, K3). +// BOUND_LIFETIME and STOP_REQUESTED were absent until 0.13 (K5, K3); GRANT_DIR +// until 0.19.0 (openkal 0.23), which conveys a grant as an inherited handle. kal_uintptr kal_process_props(void) { return KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING | KAL_PROCESS_PROP_EXIT_STATUS @@ -1020,6 +1289,7 @@ kal_uintptr kal_process_props(void) { return | KAL_PROCESS_PROP_STOP_REQUESTED // K3: the routine handles, and the word is read | KAL_PROCESS_PROP_OPEN_ID | KAL_PROCESS_PROP_ONE_STRING // 0.17.1: a program here receives one string - | KAL_PROCESS_PROP_NULL_STREAMS; } // 0.22: `NUL' at a position + | KAL_PROCESS_PROP_NULL_STREAMS // 0.22: `NUL' at a position + | KAL_PROCESS_PROP_GRANT_DIR; } // 0.23: a grant is an inherited handle } diff --git a/src/rights.cpp b/src/rights.cpp new file mode 100644 index 0000000..6214a9a --- /dev/null +++ b/src/rights.cpp @@ -0,0 +1,534 @@ +#include "rights.h" +#include "handle.h" +#include +#include + +// The rights of directory handles on this system (openkal 0.23, rights.h). +// +// THE SAME TABLE AS ON THE OTHER TWO SYSTEMS, AND TWO DIFFERENCES THAT ARE THIS +// SYSTEM'S. A handle is a pointer-sized value and not a small number, so the +// table is indexed by its slot (handle.h) and each entry keeps the handle it is +// for. And names on the volumes of this system compare without regard to case, +// spelt in more than one way: a listing answers that a name is absent only +// where no other spelling could name an entry --- a name of ASCII alone, with +// no `~' (a short name the listing does not report), in a directory whose +// entries are ASCII alone. + +namespace okw::rights { + +volatile bool g_any = false; + +namespace { + +SRWLOCK_ g_lock{}; +struct held { + held() { AcquireSRWLockExclusive(&g_lock); } + ~held() { ReleaseSRWLockExclusive(&g_lock); } +}; + +void copy(void* d, const void* s, okw_uptr n) { + auto* o = static_cast(d); + const auto* i = static_cast(s); + for (okw_uptr k = 0; k < n; ++k) o[k] = i[k]; +} +void zero(void* d, okw_uptr n) { auto* o = static_cast(d); for (okw_uptr k = 0; k < n; ++k) o[k] = 0; } + +inline bool separator(char c) { return c == '/' || c == '\\'; } +inline unsigned char folded(char c) { + const auto u = static_cast(c); + return (u >= 'A' && u <= 'Z') ? static_cast(u + 32) : u; +} + +okw_u64 hash(const char* s, okw_uptr n, okw_u64 h = 1469598103934665603ull) { + for (okw_uptr i = 0; i < n; ++i) { h ^= static_cast(s[i]); h *= 1099511628211ull; } + return h; +} +// A name as the volume compares it, for a listing. +okw_u64 folded_hash(const char* s, okw_uptr n, okw_u64 h = 1469598103934665603ull) { + for (okw_uptr i = 0; i < n; ++i) { h ^= separator(s[i]) ? '/' : folded(s[i]); h *= 1099511628211ull; } + return h; +} + +void* grab(okw_uptr n) { return kal_alloc(n, 16); } +void give_back(void* p, okw_uptr n) { if (p) kal_free(p, n, 16); } + +int resource_preference() { return kal_env_resources(); } + +// --- a set of 64-bit keys, with a byte beside each ----------------------------- +struct keyset { + okw_u64* keys = nullptr; // zero is never a key + unsigned char* vals = nullptr; + okw_uptr cap = 0, used = 0; + + bool grow() { + const okw_uptr ncap = cap ? cap * 2 : 256; + auto* nk = static_cast(grab(ncap * sizeof(okw_u64))); + auto* nv = static_cast(grab(ncap)); + if (!nk || !nv) { give_back(nk, ncap * sizeof(okw_u64)); give_back(nv, ncap); return false; } + zero(nk, ncap * sizeof(okw_u64)); + for (okw_uptr i = 0; i < cap; ++i) { + if (keys[i] == 0) continue; + okw_uptr j = keys[i] & (ncap - 1); + while (nk[j] != 0) j = (j + 1) & (ncap - 1); + nk[j] = keys[i]; nv[j] = vals[i]; + } + give_back(keys, cap * sizeof(okw_u64)); give_back(vals, cap); + keys = nk; vals = nv; cap = ncap; + return true; + } + bool add(okw_u64 k, unsigned char v) { + if (k == 0) k = 1; + if ((used + 1) * 2 > cap && !grow()) return false; + okw_uptr j = k & (cap - 1); + while (keys[j] != 0) { if (keys[j] == k) return false; j = (j + 1) & (cap - 1); } + keys[j] = k; vals[j] = v; ++used; + return true; + } + int find(okw_u64 k) const { + if (k == 0) k = 1; + if (cap == 0) return -1; + okw_uptr j = k & (cap - 1); + while (keys[j] != 0) { if (keys[j] == k) return vals[j]; j = (j + 1) & (cap - 1); } + return -1; + } + void clear() { + give_back(keys, cap * sizeof(okw_u64)); give_back(vals, cap); + keys = nullptr; vals = nullptr; cap = used = 0; + } +}; + +// --- the trees --------------------------------------------------------------- +constexpr int kRoots = 64; +constexpr okw_uptr kBatch = 64 * 1024; +constexpr unsigned char kDirectory = 1, kOther = 2; + +struct root { + bool live = false; + int refs = 0; + okw_u32 flags = 0; + void* base = nullptr; // a handle of the root, to list beneath + void* record = nullptr; // opened to append + char* rprefix = nullptr; + okw_uptr rplen = 0; + keyset seen; + char* batch = nullptr; + okw_uptr batched = 0; + keyset listed; // directory path -> 1 (settled) or 2 (cannot settle) + keyset names; // (directory path, name) -> kind + okw_uptr budget = 0; +}; +root g_roots[kRoots]; + +struct entry { + void* h = nullptr; + int root = -1; + okw_u32 flags = 0; + char* prefix = nullptr; + okw_uptr plen = 0; +}; +constexpr int kEntries = 4096; +entry g_entries[kEntries]; +unsigned short g_slot[okw::kSlots]; // entry index + 1; zero for none + +entry* entry_of(void* h) { + if (!h || !okw::within_bound(h)) return nullptr; + const unsigned short s = g_slot[okw::slot_of(h)]; + if (s == 0 || g_entries[s - 1].h != h) return nullptr; + return &g_entries[s - 1]; +} + +entry* new_entry(void* h) { + if (!h || !okw::within_bound(h)) return nullptr; + for (int i = 0; i < kEntries; ++i) + if (g_entries[i].h == nullptr) { + g_entries[i].h = h; + g_slot[okw::slot_of(h)] = static_cast(i + 1); + return &g_entries[i]; + } + return nullptr; +} + +void drop_entry(entry* e) { + if (!e) return; + if (e->h && okw::within_bound(e->h)) g_slot[okw::slot_of(e->h)] = 0; + give_back(e->prefix, e->plen + 1); + *e = entry{}; +} + +int new_root() { + for (int i = 0; i < kRoots; ++i) if (!g_roots[i].live) { g_roots[i] = root{}; g_roots[i].live = true; return i; } + return -1; +} + +okw_uptr budget_for_preference() { + switch (resource_preference()) { + case 1: return okw_uptr{1} << 20; // performance + case 2: return 0; // economy: nothing kept + default: return okw_uptr{1} << 16; // balanced + } +} + +// The path of `name' beneath the root of `e' --- after `lead', where given --- +// into `out', with `/' between components; its length, or 0 when it does not +// fit. "." components are dropped; the root itself is "" written as length 1. +okw_uptr full_path(const entry& e, const char* name, okw_uptr len, char* out, okw_uptr cap, + const char* lead = nullptr, okw_uptr lead_len = 0) { + okw_uptr o = 0; + auto put = [&](const char* s, okw_uptr n) { + if (n == 0 || (n == 1 && s[0] == '.')) return true; + if (o + n + 1 >= cap) return false; + if (o) out[o++] = '/'; + copy(out + o, s, n); o += n; + return true; + }; + auto parts = [&](const char* s, okw_uptr n) { + okw_uptr start = 0; + for (okw_uptr i = 0; s && i <= n; ++i) + if (i == n || separator(s[i])) { if (!put(s + start, i - start)) return false; start = i + 1; } + return true; + }; + if (!parts(lead, lead_len) || !parts(e.prefix, e.plen) || !parts(name, len)) return 0; + out[o] = '\0'; + return o == 0 ? 1 : o; +} + +// --- the record ---------------------------------------------------------------- +void flush(root& r) { + if (!r.record || r.batched == 0) return; + okw_uptr at = 0; + while (at < r.batched) { + DWORD wrote = 0; + if (!WriteFile(r.record, r.batch + at, static_cast(r.batched - at), &wrote, nullptr) || wrote == 0) break; + at += wrote; + } + r.batched = 0; +} + +void append(root& r, char kind, const char* path, okw_uptr n) { + if (!r.record) return; + if (!r.batch) { r.batch = static_cast(grab(kBatch)); if (!r.batch) return; } + if (n + 2 > kBatch) return; + if (r.batched + n + 2 > kBatch) flush(r); + r.batch[r.batched++] = kind; + copy(r.batch + r.batched, path, n); r.batched += n; + r.batch[r.batched++] = '\0'; +} + +// --- the listings of a tree that does not change --------------------------------- +okw_u64 name_key(okw_u64 dir, const char* s, okw_uptr n) { return folded_hash(s, n, dir ^ 0x9e3779b97f4a7c15ull); } + +bool ascii(const char* s, okw_uptr n) { + for (okw_uptr i = 0; i < n; ++i) if (static_cast(s[i]) >= 0x80) return false; + return true; +} + +// Lists `dir' (a path beneath the root, "" for the root) into the root's names. +// Answers 1 where the listing settles absence, 2 where it cannot (an entry not +// of ASCII alone), 0 where it could not be listed. +int list(root& r, const char* dir, okw_uptr dlen, okw_u64 key) { + const int was = r.listed.find(key); + if (was > 0) return was; + if (r.budget == 0) return 0; + void* h = nullptr; + { + okw::wide_name w(dir, dlen); + if (!w.ok) return 0; + okw::object_attributes attrs{}; + attrs.length = sizeof attrs; + attrs.root_directory = r.base; + attrs.object_name = &w.string; + attrs.attributes = okw::obj_case_insensitive; + okw::io_status_block status{}; + const long rc = okw::NtCreateFile(&h, FILE_LIST_DIRECTORY | SYNCHRONIZE, &attrs, &status, nullptr, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + okw::file_open, + okw::file_directory_file | okw::file_synchronous_io_nonalert, + nullptr, 0); + if (!okw::ok(rc)) return 0; + } + alignas(16) static unsigned char buf[16384]; // under the table's lock + okw_uptr count = 0; + int answer = 1; + bool first = true; + for (bool more = true; more;) { + okw::io_status_block status{}; + const long rc = okw::NtQueryDirectoryFile(h, nullptr, nullptr, nullptr, &status, buf, sizeof buf, + okw::file_directory_information_class, 0, nullptr, first ? 1 : 0); + first = false; + if (static_cast(rc) == 0x80000006u) break; // STATUS_NO_MORE_FILES + if (!okw::ok(rc)) { answer = 0; break; } + for (okw_uptr at = 0;;) { + const auto* e = reinterpret_cast(buf + at); + const okw_uptr chars = e->file_name_length / 2; + char name[1024]; + const okw_uptr n = okw::narrow(e->file_name, chars, name, sizeof name); + const bool dot = (chars == 1 && e->file_name[0] == L'.') + || (chars == 2 && e->file_name[0] == L'.' && e->file_name[1] == L'.'); + if (!dot) { + if (n == 0 || !ascii(name, n)) answer = 2; + if (count >= r.budget) { answer = 0; more = false; break; } + r.names.add(name_key(key, name, n), (e->file_attributes & FILE_ATTRIBUTE_DIRECTORY) ? kDirectory : kOther); + ++count; + } + if (e->next_entry_offset == 0) break; + at += e->next_entry_offset; + } + } + okw::NtClose(h); + if (answer == 0) return 0; + r.budget -= count; + r.listed.add(key, static_cast(answer)); + return answer; +} + +// A handle for the record of its own, opened to append: every write then lands +// at the end, whoever else holds the file, and the holder's position does not +// move. +void* reopen_to_append(void* record) { + okw::unicode_string empty{ 0, 0, nullptr }; + okw::object_attributes attrs{}; + attrs.length = sizeof attrs; + attrs.root_directory = record; + attrs.object_name = ∅ + okw::io_status_block status{}; + void* h = nullptr; + const long rc = okw::NtCreateFile(&h, FILE_APPEND_DATA | SYNCHRONIZE, &attrs, &status, nullptr, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + okw::file_open, + okw::file_non_directory_file | okw::file_synchronous_io_nonalert, + nullptr, 0); + return okw::ok(rc) ? h : nullptr; +} + +void* duplicate(void* h) { + void* out = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), h, GetCurrentProcess(), &out, 0, FALSE, DUPLICATE_SAME_ACCESS)) + return nullptr; + return out; +} + +} // namespace + +okw_u32 of(void* h) { + if (!g_any) return 0; + held l; + const entry* e = entry_of(h); + return e ? e->flags : 0; +} + +int attenuate(void* h, okw_u32 add, void* record, void** out) { + void* nh = duplicate(h); + if (!nh) return okw::translate_win32(GetLastError()); + void* rec = nullptr; + if (add & recorded) { + rec = reopen_to_append(record); + if (!rec) { okw::NtClose(nh); return kal_err_invalid; } + } + held l; + const entry* from = entry_of(h); + entry* e = new_entry(nh); + if (!e) { okw::NtClose(nh); if (rec) okw::NtClose(rec); return kal_err_no_memory; } + const okw_u32 inherited = from ? from->flags : 0; + // A NEW ROOT WHERE THERE IS SOMETHING NEW TO RECORD OR TO STATE; otherwise + // the handle is the same tree with fewer rights. + const bool new_tree = from == nullptr || (add & (recorded | unchanging)) != 0; + if (new_tree) { + const int ri = new_root(); + if (ri < 0) { drop_entry(e); okw::NtClose(nh); if (rec) okw::NtClose(rec); return kal_err_no_memory; } + root& r = g_roots[ri]; + r.flags = inherited | add; + r.refs = 1; + r.base = duplicate(nh); + r.record = rec; + r.budget = (r.flags & unchanging) ? budget_for_preference() : 0; + // A RECORD ALREADY KEPT FOR THE TREE GOES ON BEING KEPT, and its names + // stay relative to the directory it is of. + if (from && from->root >= 0 && !(add & recorded) && g_roots[from->root].record) { + const root& f = g_roots[from->root]; + r.record = duplicate(f.record); + r.flags |= recorded; + char lead[4096]; + okw_uptr n = full_path(*from, "", 0, lead, sizeof lead, f.rprefix, f.rplen); + if (n == 1 && lead[0] == '\0') n = 0; + r.rprefix = static_cast(grab(n + 1)); + if (r.rprefix) { copy(r.rprefix, lead, n); r.rprefix[n] = '\0'; r.rplen = n; } + } + e->root = ri; + e->prefix = static_cast(grab(1)); if (e->prefix) e->prefix[0] = '\0'; + e->plen = 0; + } else { + e->root = from->root; + if (e->root >= 0) g_roots[e->root].refs++; + e->prefix = static_cast(grab(from->plen + 1)); + if (e->prefix) { copy(e->prefix, from->prefix, from->plen); e->prefix[from->plen] = '\0'; } + e->plen = e->prefix ? from->plen : 0; + } + e->flags = inherited | add; + g_any = true; + *out = nh; + return kal_ok; +} + +bool derive(void* parent, void* child, const char* name, okw_uptr len) { + if (!g_any) return true; + held l; + const entry* p = entry_of(parent); + if (!p) return true; + entry* e = new_entry(child); + if (!e) return false; + e->flags = p->flags; + e->root = p->root; + if (e->root >= 0) g_roots[e->root].refs++; + char buf[4096]; + const okw_uptr n = full_path(*p, name, len, buf, sizeof buf); + const okw_uptr plen = (n == 1 && buf[0] == '\0') ? 0 : n; + e->prefix = static_cast(grab(plen + 1)); + if (e->prefix) { copy(e->prefix, buf, plen); e->prefix[plen] = '\0'; } + e->plen = e->prefix ? plen : 0; + return true; +} + +void forget(void* h) { + if (!g_any) return; + held l; + entry* e = entry_of(h); + if (!e) return; + const int ri = e->root; + drop_entry(e); + if (ri >= 0 && --g_roots[ri].refs <= 0) { + // The last handle of the tree: what it recorded is written now. The + // root itself stays until the program ends, which writes its `e'. + flush(g_roots[ri]); + g_roots[ri].names.clear(); + g_roots[ri].listed.clear(); + if (g_roots[ri].base) { okw::NtClose(g_roots[ri].base); g_roots[ri].base = nullptr; } + } +} + +void note(void* h, char kind, const char* name, okw_uptr len) { + if (!g_any) return; + held l; + const entry* e = entry_of(h); + if (!e || e->root < 0 || !(e->flags & recorded)) return; + root& r = g_roots[e->root]; + if (!r.record) return; + char buf[4096]; + okw_uptr n = full_path(*e, name, len, buf, sizeof buf, r.rprefix, r.rplen); + if (n == 0) return; + if (n == 1 && buf[0] == '\0') { buf[0] = '.'; buf[1] = '\0'; } + if (!r.seen.add(hash(buf, n, static_cast(static_cast(kind)) * 0x100000001b3ull), 1)) return; + append(r, kind, buf, n); +} + +bool known_absent(void* h, const char* name, okw_uptr len) { + if (!g_any) return false; + // A name another spelling could reach is asked of the system. + for (okw_uptr i = 0; i < len; ++i) + if (name[i] == '~' || static_cast(name[i]) >= 0x80) return false; + held l; + const entry* e = entry_of(h); + if (!e || e->root < 0 || !(e->flags & unchanging)) return false; + root& r = g_roots[e->root]; + if (!r.base) return false; + char path[4096]; + const okw_uptr n = full_path(*e, name, len, path, sizeof path); + if (n == 0 || (n == 1 && path[0] == '\0')) return false; + okw_uptr start = 0; + okw_u64 dir = folded_hash("", 0); + for (okw_uptr i = 0; i <= n; ++i) { + if (i < n && path[i] != '/') continue; + if (list(r, path, start == 0 ? 0 : start - 1, dir) != 1) return false; + const int kind = r.names.find(name_key(dir, path + start, i - start)); + if (kind < 0) return true; + if (i == n) return false; + if (kind != kDirectory) return false; + dir = folded_hash(path, i); + start = i + 1; + } + return false; +} + +void changed(void* h) { + if (!g_any) return; + held l; + const entry* e = entry_of(h); + if (!e || e->root < 0) return; + root& r = g_roots[e->root]; + r.names.clear(); + r.listed.clear(); + if (r.flags & unchanging) r.budget = budget_for_preference(); +} + +bool conveyed(void* h, okw_u32* flags, void** record, const char** prefix, okw_uptr* prefix_len) { + if (!g_any) return false; + held l; + const entry* e = entry_of(h); + if (!e) return false; + *flags = e->flags; + *record = (e->root >= 0 && (e->flags & recorded)) ? g_roots[e->root].record : nullptr; + static char lead[4096]; + okw_uptr n = 0; + if (e->root >= 0) { + const root& r = g_roots[e->root]; + n = full_path(*e, "", 0, lead, sizeof lead, r.rprefix, r.rplen); + if (n == 1 && lead[0] == '\0') n = 0; + } + *prefix = lead; + *prefix_len = n; + return true; +} + +void adopt(void* h, okw_u32 flags, void* record, const char* prefix, okw_uptr prefix_len) { + held l; + entry* e = new_entry(h); + if (!e) return; + const int ri = new_root(); + if (ri < 0) { drop_entry(e); return; } + root& r = g_roots[ri]; + r.flags = flags; + r.refs = 1; + r.base = duplicate(h); + if (record) { + // Not inherited by the programs this one starts, as the grant itself + // is not, unless it grants the handle on. + SetHandleInformation(record, HANDLE_FLAG_INHERIT, 0); + r.record = record; + } + r.budget = (flags & unchanging) ? budget_for_preference() : 0; + r.rprefix = static_cast(grab(prefix_len + 1)); + if (r.rprefix) { copy(r.rprefix, prefix, prefix_len); r.rprefix[prefix_len] = '\0'; r.rplen = prefix_len; } + e->root = ri; + e->flags = flags; + e->prefix = static_cast(grab(1)); + if (e->prefix) e->prefix[0] = '\0'; + e->plen = 0; + g_any = true; +} + +void end_of_program() { + if (!g_any) return; + static volatile long long ended = 0; + if (okw::atomic::exchange(&ended, 1)) return; + held l; + for (auto& r : g_roots) { + if (!r.live || !r.record) continue; + if (!r.batch) r.batch = static_cast(grab(kBatch)); + if (r.batch) { + if (r.batched + 2 > kBatch) flush(r); + r.batch[r.batched++] = 'e'; + r.batch[r.batched++] = '\0'; + } + flush(r); + } +} + +} // namespace okw::rights + +namespace { +// A program whose runtime ends it without kal_exit --- this system's own C +// runtime --- writes its records as that runtime runs its terminators. +OKW_END_FN(records_at_exit) { okw::rights::end_of_program(); } +OKW_AT_END(records_at_exit); +} // namespace diff --git a/src/rights.h b/src/rights.h new file mode 100644 index 0000000..3e28bb2 --- /dev/null +++ b/src/rights.h @@ -0,0 +1,58 @@ +// A directory handle with less (openkal 0.23, `kal_fs_dir_attenuate', +// SPEC.md clause 7.16): what a handle may not do, what its holder stated, and +// what it records. +// +// A table indexed by the handle's slot (handle.h), consulted by every operation +// of openkal.fs that takes a directory. It is empty in a program that +// attenuates nothing, and the one test of `g_any' is then all an operation +// pays. +#pragma once +#include "win.h" + +namespace okw::rights { + +inline constexpr okw_u32 read_only = 1u; // KAL_DIR_READ_ONLY +inline constexpr okw_u32 unchanging = 2u; // KAL_DIR_UNCHANGING +inline constexpr okw_u32 recorded = 4u; // KAL_DIR_RECORDED + +extern volatile bool g_any; + +// The rights a directory handle carries; zero for one that carries none. +okw_u32 of(void* h); + +// A new handle for the directory `h' names, carrying the rights of `h' and +// `add'; `record' is the holder's file for `recorded'. Returns a kal_err value, +// and writes the new handle to `out' on kal_ok. +int attenuate(void* h, okw_u32 add, void* record, void** out); + +// A directory `child' opened beneath `parent' by `name': it carries what the +// parent carries. Returns false when the table has no room, and the caller then +// refuses the open rather than hand out a directory with more than its base. +bool derive(void* parent, void* child, const char* name, okw_uptr len); + +// The handle is being closed. +void forget(void* h); + +// What a name reached beneath `h' was found to be, for a recorded handle: +// 'r' read, 'w' written, 's' asked about and present, 'a' absent, 'l' listed. +// A name of length zero is the directory itself. +void note(void* h, char kind, const char* name, okw_uptr len); + +// Whether a handle that states nothing changes beneath it knows `name' to be +// absent, from what it has listed --- without asking the system. +bool known_absent(void* h, const char* name, okw_uptr len); + +// Something beneath was changed through a handle of this tree: what was listed +// may no longer hold. +void changed(void* h); + +// THE CONVEYANCE TO A STARTED PROGRAM. What a granted handle carries, for the +// variable a start writes; and, in the started program, a preopen received +// with rights. +bool conveyed(void* h, okw_u32* flags, void** record, const char** prefix, okw_uptr* prefix_len); +void adopt(void* h, okw_u32 flags, void* record, const char* prefix, okw_uptr prefix_len); + +// The program is ending: every record is written, and ended with `e'. +void end_of_program(); + +} // namespace okw::rights diff --git a/src/timeout.cpp b/src/timeout.cpp index c7ba6b3..5f0b81a 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -261,14 +261,11 @@ int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* termin if (r == WAIT_TIMEOUT_) return kal_err_again; if (r != WAIT_OBJECT_0) return okw::translate_win32(GetLastError()); - DWORD code = 0; - if (!GetExitCodeProcess(h, &code)) return okw::translate_win32(GetLastError()); - if (status) *status = static_cast(code); - // This system does not distinguish a program that ended by returning from - // one the environment ended: both are an exit code, and process.cpp reports - // the same thing. - if (terminated) *terminated = 0; - return kal_ok; + // The program has ended, and what it ended with is read as an unbounded + // wait reads it (process.cpp), so that the two report one program alike: + // until 0.19.0 this one called every end a returned number, where the + // other told the numbers this system gives a program it ended apart. + return kal_process_wait(p, status, terminated); } // The bound this system distinguishes. Both `WSAPoll' and the wait upon an diff --git a/src/version.cpp b/src/version.cpp index ba2bf26..1d9f2d0 100644 --- a/src/version.cpp +++ b/src/version.cpp @@ -17,7 +17,10 @@ kal_u64 kal_interfaces(void) { | KAL_IFACE_FS | KAL_IFACE_PROCESS | KAL_IFACE_TASK | KAL_IFACE_EXEC | KAL_IFACE_TERMINAL | KAL_IFACE_NET | KAL_IFACE_DATAGRAM | KAL_IFACE_TIMEOUT | KAL_IFACE_MAP - | KAL_IFACE_REGION; + | KAL_IFACE_REGION + // openkal.prof and openkal.debug for a program of this environment + // (0.23): samples and a fault's report (observe.cpp). + | KAL_IFACE_PROF | KAL_IFACE_DEBUG; } } diff --git a/src/win.cpp b/src/win.cpp index b6ede5d..30bda27 100644 --- a/src/win.cpp +++ b/src/win.cpp @@ -27,6 +27,13 @@ namespace okw { okw_uptr length(const char* s) { okw_uptr n = 0; while (s && s[n]) ++n; return n; } +// Both libraries are mapped into every program of this system before its first +// instruction, so loading one by name only finds it. +void* system_function(const wchar_t* library, const char* name) { + HANDLE lib = LoadLibraryW(library); + return lib ? GetProcAddress(lib, name) : nullptr; +} + int translate_win32(unsigned long e) { switch (e) { case ERROR_SUCCESS: return kal_ok; diff --git a/src/win.h b/src/win.h index 6556f75..f6ec23e 100644 --- a/src/win.h +++ b/src/win.h @@ -49,8 +49,62 @@ using okw_u64 = unsigned long long; using okw_i64 = long long; using okw_u32 = unsigned int; +// WHAT RUNS WHEN A PROGRAM STARTS OR ENDS (0.23), WITHOUT A C RUNTIME SYMBOL. +// +// A function listed in the section the program's runtime walks at its start or +// at its end: `.ctors'/`.dtors' on the GNU ABI (the C library above walks them +// where it is the runtime), the CRT's own tables on the other. Neither names a +// symbol of a C runtime; the second compiler of that ABI reads the attribute +// the first two read as the same tables. +#if defined(_MSC_VER) && !defined(__clang__) +#pragma section(".CRT$XCU", read) +#pragma section(".CRT$XTU", read) +#define OKW_AT_START(f) __declspec(allocate(".CRT$XCU")) static void (*f##_at_start)(void) = f +#define OKW_AT_END(f) __declspec(allocate(".CRT$XTU")) static void (*f##_at_end)(void) = f +#define OKW_START_FN(f) void f() +#define OKW_END_FN(f) void f() +#else +#define OKW_AT_START(f) static_assert(true, "") +#define OKW_AT_END(f) static_assert(true, "") +#define OKW_START_FN(f) [[gnu::constructor]] void f() +#define OKW_END_FN(f) [[gnu::destructor]] void f() +#endif + +// ONE WORD READ AND WRITTEN BY SEVERAL CONTEXTS (0.23: the sampler's ring, a +// flag set once). The compilers of the GNU ABI and the second compiler of the +// other share one spelling; the first compiler of the other spells the same +// instructions as its intrinsics, which are instructions and not a library. +#if defined(_MSC_VER) && !defined(__clang__) +extern "C" long long _InterlockedExchange64(volatile long long*, long long); +extern "C" long long _InterlockedExchangeAdd64(volatile long long*, long long); +extern "C" long long _InterlockedOr64(volatile long long*, long long); +#pragma intrinsic(_InterlockedExchange64, _InterlockedExchangeAdd64, _InterlockedOr64) +namespace okw::atomic { +inline long long exchange(volatile long long* p, long long v) { return _InterlockedExchange64(p, v); } +inline long long fetch_add(volatile long long* p, long long v) { return _InterlockedExchangeAdd64(p, v); } +inline long long load(volatile long long* p) { return _InterlockedOr64(p, 0); } +inline void store(volatile long long* p, long long v) { (void)_InterlockedExchange64(p, v); } +inline long long fetch_or(volatile long long* p, long long v) { return _InterlockedOr64(p, v); } +} // namespace okw::atomic +#else +namespace okw::atomic { +inline long long exchange(volatile long long* p, long long v) { return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL); } +inline long long fetch_add(volatile long long* p, long long v) { return __atomic_fetch_add(p, v, __ATOMIC_ACQ_REL); } +inline long long load(volatile long long* p) { return __atomic_load_n(p, __ATOMIC_ACQUIRE); } +inline void store(volatile long long* p, long long v) { __atomic_store_n(p, v, __ATOMIC_RELEASE); } +inline long long fetch_or(volatile long long* p, long long v) { return __atomic_fetch_or(p, v, __ATOMIC_ACQ_REL); } +} // namespace okw::atomic +#endif + namespace okw { +// A name of this system's newer libraries, reached at run time (win32.h); +// null where the library or the name is absent. +void* system_function(const wchar_t* library, const char* name); + +// A program's pages made the first this system takes back (economy, 0.23). +void lower_memory_priority(void* process); + // --- the object manager ------------------------------------------------------ // // Declared here rather than taken from : the header is present in @@ -122,6 +176,21 @@ struct file_directory_information { wchar_t file_name[1]; }; +// The same record with the file's identity before its name (0.23): the index +// file_internal_information reports, given for each entry of a listing. +struct file_id_full_dir_information { + unsigned long next_entry_offset; + unsigned long file_index; + okw_i64 creation_time, last_access_time, last_write_time, change_time; + okw_i64 end_of_file, allocation_size; + unsigned long file_attributes; + unsigned long file_name_length; // in bytes + unsigned long ea_size; + okw_i64 file_id; + wchar_t file_name[1]; +}; +static_assert(sizeof(okw_i64) == 8 && alignof(okw_i64) == 8, "the record's identity is at 72"); + // The index this environment keeps for a file, which is unique within a volume. struct file_internal_information { okw_i64 index_number; }; @@ -147,6 +216,7 @@ enum : unsigned long { fs_case_sensitive_search = 0x00000001u, fs_supports_reparse_points = 0x00000080u, fs_supports_posix_unlink_rename = 0x00000400u, + fs_supports_open_by_file_id = 0x01000000u, }; enum : int { @@ -160,6 +230,7 @@ enum : int { file_disposition_information_ex_class = 64, file_end_of_file_information_class = 20, file_rename_information_class = 10, + file_id_full_directory_information_class = 38, }; // The classes NtQueryVolumeInformationFile takes. diff --git a/src/win32.h b/src/win32.h index 6fa9966..92f12a2 100644 --- a/src/win32.h +++ b/src/win32.h @@ -426,6 +426,10 @@ OKW_IMPORT BOOL OKW_API WaitOnAddress(volatile void*, void*, unsigned long lon struct SRWLOCK_ { LPVOID Ptr; }; OKW_IMPORT void OKW_API AcquireSRWLockExclusive(SRWLOCK_*); OKW_IMPORT void OKW_API ReleaseSRWLockExclusive(SRWLOCK_*); +// Shared, and without waiting, for a fault's report (0.23): a context that +// faulted while another held the lock reports less rather than not at all. +OKW_IMPORT unsigned char OKW_API TryAcquireSRWLockShared(SRWLOCK_*); +OKW_IMPORT void OKW_API ReleaseSRWLockShared(SRWLOCK_*); OKW_IMPORT void OKW_API WakeByAddressSingle(void*); OKW_IMPORT void OKW_API WakeByAddressAll(void*); @@ -579,15 +583,125 @@ OKW_IMPORT BOOL OKW_API K32GetProcessMemoryInfo(HANDLE, PROCESS_MEMORY_COUNTER OKW_IMPORT HANDLE OKW_API CreateIoCompletionPort(HANDLE, HANDLE, unsigned long long, DWORD); OKW_IMPORT BOOL OKW_API GetQueuedCompletionStatus(HANDLE, DWORD*, unsigned long long*, OVERLAPPED**, DWORD); +// ── openkal 0.23 ──────────────────────────────────────────────────────────── +// +// A handle granted to a started program is a duplicate made inheritable for +// the one start, and named in the start's explicit list (kal_process_spawn). +OKW_IMPORT BOOL OKW_API DuplicateHandle(HANDLE, HANDLE, HANDLE, HANDLE*, DWORD, BOOL, DWORD); +inline constexpr DWORD DUPLICATE_SAME_ACCESS = 0x2u; + +// What the machine has, and what it can give now (kal_memory_info). +struct MEMORYSTATUSEX_ { + DWORD dwLength; + DWORD dwMemoryLoad; + unsigned long long ullTotalPhys, ullAvailPhys, ullTotalPageFile, ullAvailPageFile; + unsigned long long ullTotalVirtual, ullAvailVirtual, ullAvailExtendedVirtual; +}; +static_assert(sizeof(MEMORYSTATUSEX_) == 64, "MEMORYSTATUSEX"); +OKW_IMPORT BOOL OKW_API GlobalMemoryStatusEx(MEMORYSTATUSEX_*); + +// Where an address lies, and what is there (kal_exec_*, the fault report). +struct MEMORY_BASIC_INFORMATION_ { + LPVOID BaseAddress; + LPVOID AllocationBase; + DWORD AllocationProtect; + WORD PartitionId; + unsigned long long RegionSize; + DWORD State; + DWORD Protect; + DWORD Type; +}; +static_assert(sizeof(MEMORY_BASIC_INFORMATION_) == 48, "MEMORY_BASIC_INFORMATION"); +OKW_IMPORT unsigned long long OKW_API VirtualQuery(LPCVOID, MEMORY_BASIC_INFORMATION_*, unsigned long long); +inline constexpr DWORD MEM_IMAGE_ = 0x1000000u; +inline constexpr DWORD PAGE_NOACCESS = 0x01u; +inline constexpr DWORD PAGE_EXECUTE_READWRITE = 0x40u; +inline constexpr DWORD FILE_EXECUTE_ = 0x20u; +// Memory read where it may not be there: a frame of a broken chain is refused +// rather than read (the fault report, samples). +OKW_IMPORT BOOL OKW_API ReadProcessMemory(HANDLE, LPCVOID, LPVOID, unsigned long long, unsigned long long*); +// The file a view of a file maps, in the object manager's spelling of its name. +OKW_IMPORT DWORD OKW_API K32GetMappedFileNameW(HANDLE, LPVOID, LPWSTR, DWORD); + +// THE FAULT REPORT is the filter this system calls for an exception no +// handler took --- a fault that ends the program --- and not a first look at +// every exception: a program that takes an access of its own on purpose +// (a guard page, a probe) is not reported. +struct EXCEPTION_RECORD_ { + DWORD ExceptionCode; + DWORD ExceptionFlags; + EXCEPTION_RECORD_* ExceptionRecord; + LPVOID ExceptionAddress; + DWORD NumberParameters; + unsigned long long ExceptionInformation[15]; +}; +struct EXCEPTION_POINTERS_ { EXCEPTION_RECORD_* ExceptionRecord; void* ContextRecord; }; +using TOP_LEVEL_FILTER_ = long (OKW_API*)(EXCEPTION_POINTERS_*); +OKW_IMPORT TOP_LEVEL_FILTER_ OKW_API SetUnhandledExceptionFilter(TOP_LEVEL_FILTER_); +// TOUCHES: a first look at the one exception that is a touch being recorded --- +// an execution of a page made not executable for it --- which is resolved and +// the program continues; every other exception passes on. +OKW_IMPORT LPVOID OKW_API AddVectoredExceptionHandler(ULONG, TOP_LEVEL_FILTER_); +OKW_IMPORT ULONG OKW_API RemoveVectoredExceptionHandler(LPVOID); + +// SAMPLES: a context of this program's own that suspends each of the others +// and reads where it stands. +OKW_IMPORT HANDLE OKW_API OpenThread(DWORD, BOOL, DWORD); +OKW_IMPORT DWORD OKW_API SuspendThread(HANDLE); +OKW_IMPORT BOOL OKW_API GetThreadContext(HANDLE, void*); +OKW_IMPORT BOOL OKW_API QueryThreadCycleTime(HANDLE, unsigned long long*); +OKW_IMPORT BOOL OKW_API GetThreadTimes(HANDLE, FILETIME*, FILETIME*, FILETIME*, FILETIME*); +OKW_IMPORT HANDLE OKW_API CreateToolhelp32Snapshot(DWORD, DWORD); +struct THREADENTRY32_ { + DWORD dwSize, cntUsage, th32ThreadID, th32OwnerProcessID; + LONG tpBasePri, tpDeltaPri; + DWORD dwFlags; +}; +OKW_IMPORT BOOL OKW_API Thread32First(HANDLE, THREADENTRY32_*); +OKW_IMPORT BOOL OKW_API Thread32Next(HANDLE, THREADENTRY32_*); +OKW_IMPORT HANDLE OKW_API CreateWaitableTimerExW(SECURITY_ATTRIBUTES*, LPCWSTR, DWORD, DWORD); +OKW_IMPORT BOOL OKW_API SetWaitableTimer(HANDLE, const LARGE_INTEGER*, LONG, void*, LPVOID, BOOL); +inline constexpr DWORD THREAD_SUSPEND_RESUME_ = 0x0002u; +inline constexpr DWORD THREAD_GET_CONTEXT_ = 0x0008u; +inline constexpr DWORD THREAD_QUERY_LIMITED_INFORMATION_ = 0x0800u; +inline constexpr DWORD TH32CS_SNAPTHREAD_ = 0x00000004u; +inline constexpr DWORD CREATE_WAITABLE_TIMER_HIGH_RESOLUTION_ = 0x00000002u; +inline constexpr DWORD TIMER_ALL_ACCESS_ = 0x001F0003u; + // ── ntdll ─────────────────────────────────────────────────────────────────── // // The object-manager entry points. Their STRUCTURES are declared in win.h and // have been since this package was written, for the reason recorded there; // these are the calls that take them. OKW_IMPORT DWORD OKW_API RtlNtStatusToDosError(long); +// The program that started this one, for a grant's variable (0.23): the record +// this system keeps of a process, whose sixth word is its parent's identifier. +OKW_IMPORT long OKW_API NtQueryInformationProcess(HANDLE, int, LPVOID, ULONG, ULONG*); } // extern "C" +// ── reached at run time: what a system of a given version has ─────────────── +// +// THE PLACEHOLDERS OF A RESERVATION (Windows 10 1803), the enquiry by name +// (1709), the gathering of a range's pages (8) and a program's memory priority +// (8) are in this system's newer libraries. Taken from the library at the first +// use, so that a program built here starts on a system without them; where a +// name is absent, the position it would answer is not claimed. +using pfn_VirtualAlloc2_ = LPVOID (OKW_API*)(HANDLE, LPVOID, unsigned long long, ULONG, ULONG, void*, ULONG); +using pfn_MapViewOfFile3_ = LPVOID (OKW_API*)(HANDLE, HANDLE, LPVOID, unsigned long long, unsigned long long, + ULONG, ULONG, void*, ULONG); +using pfn_UnmapViewOfFile2_ = BOOL (OKW_API*)(HANDLE, LPVOID, ULONG); +using pfn_VirtualFreeEx_ = BOOL (OKW_API*)(HANDLE, LPVOID, unsigned long long, DWORD); +struct WIN32_MEMORY_RANGE_ENTRY_ { LPVOID VirtualAddress; unsigned long long NumberOfBytes; }; +using pfn_PrefetchVirtualMemory_ = BOOL (OKW_API*)(HANDLE, unsigned long long, WIN32_MEMORY_RANGE_ENTRY_*, ULONG); +using pfn_SetProcessInformation_ = BOOL (OKW_API*)(HANDLE, int, LPVOID, DWORD); +using pfn_NtQueryInformationByName_ = long (OKW_API*)(void* attributes, void* status, void* info, ULONG length, int cls); +// Constants and not enumerators: each is combined with this file's older +// constants, and two enumerations are not combined. +inline constexpr DWORD MEM_RESERVE_PLACEHOLDER_ = 0x00040000u; +inline constexpr DWORD MEM_REPLACE_PLACEHOLDER_ = 0x00004000u; +inline constexpr DWORD MEM_PRESERVE_PLACEHOLDER_ = 0x00000002u; + // ── ws2_32: this system's network interface ───────────────────────────────── // // NOT DECLARED AS IMPORTS AND NOT LINKED, AND THE REASON IS A COLLISION diff --git a/tests/openkal_0_23.cpp b/tests/openkal_0_23.cpp new file mode 100644 index 0000000..e837c8f --- /dev/null +++ b/tests/openkal_0_23.cpp @@ -0,0 +1,285 @@ +// What openkal-windows 0.19 does beside openkal 0.23, as this system does it. +// +// A GRANT ARRIVES AS AN INHERITED HANDLE: the started program has the granted +// directories and nothing else, holds them uninheritable, and a program it +// starts without grants --- the variable naming them inherited all the same --- +// has the directories a program has by default, because the variable names the +// program that wrote it. A RECORD ACROSS A START: a program granted a recorded +// handle appends what it reached to its caller's record, and its end. A PART +// OF A REGION THAT IS A FILE'S leaves the rest of the region as it was, and a +// part this system cannot place a view at is refused with the region +// unchanged. A PREFERENCE FOR ECONOMY lowers the started program's memory +// priority, and one for performance leaves it the system's --- the starter's, +// which a started program has. A FAULT is reported by what ends the program, naming the program's +// own file. +#include +#include +#include "../src/win32.h" +#include "../src/handle.h" +#include +#include +#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; +} + +kal_uintptr length(const char* s) { return std::strlen(s); } + +// This program's volume among the preopens ("X:"), and its name beneath it. +char g_self[1024]; +char g_dir[1024]; // the directory it is in, beneath the volume +char g_volume_name[3]; + +bool locate_self(kal_dir* volume) { + char image[1024]; + const kal_intptr n = kal_env_image(image, sizeof image - 1); + if (n < 4 || image[1] != ':' || (image[2] != '\\' && image[2] != '/')) return false; + image[n] = '\0'; + for (kal_intptr i = 3; i <= n; ++i) g_self[i - 3] = image[i] == '\\' ? '/' : image[i]; + std::strcpy(g_dir, g_self); + char* slash = std::strrchr(g_dir, '/'); + if (slash) *slash = '\0'; else g_dir[0] = '\0'; + const kal_uintptr count = kal_fs_preopen_count(); + for (kal_uintptr i = 0; i < count; ++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] == ':' && (name[0] | 0x20) == (image[0] | 0x20)) { + *volume = d; + g_volume_name[0] = name[0]; g_volume_name[1] = ':'; g_volume_name[2] = '\0'; + return true; + } + } + return false; +} + +kal_dir by_name(const char* want) { + for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { + char name[64]{}; kal_uintptr len = 0; kal_dir d{}; + if (kal_fs_preopen(i, &d, name, sizeof name, &len) == kal_ok && len == length(want) + && std::memcmp(name, want, len) == 0) + return d; + } + return kal_dir{}; +} + +// Starts this program with `errand', from the volume named `volume_name' among +// the caller's preopens, and returns its status, or -1. +int start(kal_dir volume, const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uintptr flags, + const kal_spawn_streams* streams = nullptr, bool null_grants = false) { + const char* argv[2] = { "openkal_0_23", errand }; + const kal_uintptr lens[2] = { length(argv[0]), length(errand) }; + const kal_spawn how{ volume, volume, nullptr, null_grants ? nullptr : grants, n, flags }; + kal_process p{}; + if (kal_process_spawn(&how, g_self, length(g_self), argv, lens, 2, nullptr, nullptr, 0, streams, &p) != kal_ok) + return -1; + int status = -1, terminated = 0; + kal_process_wait(p, &status, &terminated); + kal_process_close(p); + return terminated ? -2 : status; +} + +bool holds(const char* buf, long got, const char* entry) { + const long len = static_cast(length(entry)) + 1; + for (long i = 0; i + len <= got; ++i) + if (std::memcmp(buf + i, entry, static_cast(len)) == 0 && (i == 0 || buf[i - 1] == 0)) return true; + return false; +} + +[[gnu::noinline]] void fault_here() { *static_cast(nullptr) = 1; } + +// This program's memory priority (0 to 5; MEMORY_PRIORITY_LOW is 2), or -1 +// where the system does not report it. +int memory_priority() { + using get_t = BOOL (OKW_API*)(HANDLE, int, LPVOID, DWORD); + const auto get = reinterpret_cast(okw::system_function(L"kernel32.dll", "GetProcessInformation")); + unsigned long priority = 0; + if (!get || !get(GetCurrentProcess(), 0 /* ProcessMemoryPriority */, &priority, sizeof priority)) return -1; + return static_cast(priority); +} + +} // namespace + +int main(int argc, char** argv) { + const char* errand = argc > 1 ? argv[1] : ""; + + if (std::strcmp(errand, "--read") == 0) { + // The recorded grant is the first preopen: a file read, a name absent, + // and the end through kal_exit, which ends the record. + kal_dir d = by_name("/granted"); + kal_file f{}; + if (kal_fs_open(d, "present.txt", 11, KAL_OPEN_READ, &f) != kal_ok) kal_exit(52); + kal_fs_close_file(f); + kal_node_info i{}; i.self_size = sizeof i; + kal_fs_info(d, "absent.txt", 10, 0, KAL_INFO_KIND, &i); + kal_exit(0); + } + if (std::strcmp(errand, "--grants") == 0) { + if (kal_fs_preopen_count() != 2) return 41; + kal_dir own_volume{}; + if (!locate_self(&own_volume)) return 47; + kal_dir granted = by_name("/granted"); + if (!granted.h) return 42; + DWORD flags = 1; + if (!GetHandleInformation(okw::unpack(granted.h), &flags) || (flags & HANDLE_FLAG_INHERIT)) return 43; + // A program this one starts without grants, the variable inherited. + kal_dir volume{}; + for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { + char name[16]{}; kal_uintptr len = 0; kal_dir d{}; + if (kal_fs_preopen(i, &d, name, sizeof name, &len) == kal_ok && len == 2 && name[1] == ':') volume = d; + } + if (!volume.h) return 44; + return start(volume, "--defaults", nullptr, 0, 0, nullptr, true) == 0 ? 0 : 45; + } + if (std::strcmp(errand, "--defaults") == 0) + return kal_fs_preopen_count() > 2 && by_name("/granted").h == 0 ? 0 : 46; + if (std::strcmp(errand, "--priority") == 0) { + const int p = memory_priority(); + return p < 0 ? 69 : 70 + p; + } + if (std::strcmp(errand, "--fault") == 0) { fault_here(); return 0; } + + kal_dir volume{}; + if (!locate_self(&volume)) { + std::printf("SKIP: this program's own name could not be expressed against a volume\n"); + return 0; + } + char tmp[1100]; + std::snprintf(tmp, sizeof tmp, "%s/okw-0.23.tmp", g_dir); + kal_fs_mkdir(volume, tmp, length(tmp)); + kal_dir dir{}; + check(kal_fs_open_dir(volume, tmp, length(tmp), &dir) == kal_ok, "a directory for the test is opened"); + { + kal_file f{}; + if (kal_fs_open(dir, "present.txt", 11, KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE, &f) == kal_ok) { + kal_stream_write(kal_fs_stream(f), "x", 1); + kal_fs_close_file(f); + } + } + + // GRANTS. + check((kal_process_props() & KAL_PROCESS_PROP_GRANT_DIR) != 0, "the directory grant is claimed"); + { + const kal_preopen grants[2] = { { dir, "/granted", 8 }, { volume, g_volume_name, 2 } }; + check(start(volume, "--grants", grants, 2, 0) == 0, + "the started program has its grants, uninheritable, and one it starts without grants has the defaults"); + } + + // A RECORD ACROSS A START. + { + kal_file rec{}; + check(kal_fs_open(dir, "record", 6, KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE, &rec) == kal_ok, + "a record file is opened"); + kal_dir_rights rights{}; + rights.self_size = sizeof rights; + rights.flags = KAL_DIR_RECORDED | KAL_DIR_READ_ONLY; + rights.record = rec; + kal_dir rd{}; + check(kal_fs_dir_attenuate(dir, &rights, &rd) == kal_ok, "the directory is attenuated, recorded"); + const kal_preopen grants[2] = { { rd, "/granted", 8 }, { volume, g_volume_name, 2 } }; + check(start(volume, "--read", grants, 2, 0) == 0, "a program granted the recorded handle reads through it"); + kal_fs_close_dir(rd); + kal_fs_close_file(rec); + char buf[4096] = {}; + long got = 0; + kal_file r{}; + if (kal_fs_open(dir, "record", 6, KAL_OPEN_READ, &r) == kal_ok) { + const kal_intptr n = kal_stream_read(kal_fs_stream(r), buf, sizeof buf); + got = n > 0 ? static_cast(n) : 0; + kal_fs_close_file(r); + } + check(holds(buf, got, "rpresent.txt"), "the started program's read is in its caller's record"); + check(holds(buf, got, "aabsent.txt"), "and so is the name it found absent"); + check(holds(buf, got, "e"), "and the record of the program that ended is ended"); + } + + // A PART OF A REGION THAT IS A FILE'S. + if (kal_exec_props() & KAL_EXEC_PROP_FILE) { + constexpr kal_uintptr block = 65536; + kal_file f{}; + bool written = kal_fs_open(dir, "part.bin", 8, KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE, &f) == kal_ok; + static unsigned char bytes[block]; + for (kal_uintptr i = 0; i < block; ++i) bytes[i] = static_cast(i * 7 + 1); + written = written && kal_stream_write(kal_fs_stream(f), bytes, block) == static_cast(block); + if (f.h) kal_fs_close_file(f); + kal_file rf{}; + auto* r = static_cast(kal_exec_alloc(3 * block)); + if (written && r && kal_fs_open(dir, "part.bin", 8, KAL_OPEN_READ, &rf) == kal_ok) { + for (kal_uintptr i = 0; i < block; ++i) { r[i] = static_cast(i + 3); r[2 * block + i] = static_cast(i + 5); } + r[block] = 9; + check(kal_exec_map_part(r, 4096, 4096, rf, 0, 0) == kal_err_not_supported && r[block] == 9, + "a part this system cannot place a view at is refused, and the region is unchanged"); + check(kal_exec_map_part(r, block, block, rf, 0, 0) == kal_ok, "a part of the region is made the file's"); + bool rest = true, part = true; + for (kal_uintptr i = 0; i < block; ++i) { + rest = rest && r[i] == static_cast(i + 3) && r[2 * block + i] == static_cast(i + 5); + part = part && r[block + i] == bytes[i]; + } + check(part, "the part holds the file's bytes"); + check(rest, "and the rest of the region holds what was written to it"); + check(kal_exec_seal_part(r, 0, 3 * block, 0) == kal_ok && r[0] == 3 && r[2 * block] == 5, + "the region is sealed across its views, and reads as before"); + kal_fs_close_file(rf); + } + if (r) kal_exec_free(r, 3 * block); + kal_fs_remove(dir, "part.bin", 8); + } else { + std::printf(" this system has no placeholders; a part of a region is not made a file's\n"); + } + + // ECONOMY. + { + const int mine = memory_priority(); + const int low = start(volume, "--priority", nullptr, 0, KAL_SPAWN_RESOURCES_ECONOMY, nullptr, true); + if (low == 69 || mine < 0) std::printf(" this system does not report a program's memory priority\n"); + else { + check(low == 72, "a program started for economy has a low memory priority"); + check(start(volume, "--priority", nullptr, 0, KAL_SPAWN_RESOURCES_PERFORMANCE, nullptr, true) == 70 + mine, + "and one started for performance has the system's, its starter's"); + } + } + + // A FAULT. + { + kal_stream mine{}, theirs{}; + if (kal_process_channel(&mine, &theirs) == kal_ok) { + const kal_spawn_streams streams{ {0}, {0}, theirs }; + const int s = start(volume, "--fault", nullptr, 0, 0, &streams, true); + kal_process_channel_close(theirs); + static char report[16384]; + kal_uintptr have = 0; + for (;;) { + const kal_intptr r = kal_stream_read(mine, report + have, sizeof report - 1 - have); + if (r <= 0) break; + have += static_cast(r); + } + kal_process_channel_close(mine); + report[have] = '\0'; + check(s == -2, "a program that faults is reported as ended by the system"); + check(std::strstr(report, "openkal: fault: ") != nullptr && std::strstr(report, "openkal: #0 ") != nullptr, + "and what ends it reports the fault and its frames"); + const char* base = std::strrchr(g_self, '/'); + check(base && std::strstr(report, base + 1) != nullptr, "and names the program's own file"); +#if defined(__MINGW32__) + // A linker of this ABI keeps the symbol table in the file. + check(std::strstr(report, "fault_here") != nullptr, "and the function that faulted"); +#endif + } + } + + kal_fs_remove(dir, "present.txt", 11); + kal_fs_remove(dir, "record", 6); + kal_fs_close_dir(dir); + kal_fs_remove(volume, tmp, length(tmp)); + std::printf("openkal-windows: openkal 0.23\n"); + return failures == 0 ? 0 : 1; +} diff --git a/tests/what_a_started_program_receives.cpp b/tests/what_a_started_program_receives.cpp index d7c4961..289d3f0 100644 --- a/tests/what_a_started_program_receives.cpp +++ b/tests/what_a_started_program_receives.cpp @@ -15,10 +15,9 @@ // CreateProcessW by different paths: inheritance disabled, and inheritance // enabled and narrowed to an explicit list. // -// It also observes that a grant is refused. This environment has no numbering a -// preopen could arrive under, KAL_PROCESS_PROP_GRANT_DIR is not claimed, and a -// program started without the directories it was asked to receive is not the -// program the caller asked for (clause 6.2). +// A grant is a directory handle the caller places on purpose, and reaches the +// started program by the same list (from 0.19.0, openkal 0.23); what it +// receives is observed in tests/openkal_0_23.cpp. #include #include #include @@ -149,20 +148,6 @@ int main(int argc, char** argv) { observe(volume, self, true, "an unplaced inheritable handle does not reach a program started with a placed stream"); - // A grant this implementation does not claim is refused, not ignored. - check((kal_process_props() & KAL_PROCESS_PROP_GRANT_DIR) == 0, - "the directory grant is not claimed"); - { - const kal_preopen grant{ volume, "x", 1 }; - const kal_spawn how{ volume, volume, nullptr, &grant, 1, 0 }; - const char* argv1[1] = { "x" }; - const kal_uintptr lens1[1] = { 1 }; - kal_process p{}; - check(kal_process_spawn(&how, "x", 1, argv1, lens1, 1, nullptr, nullptr, 0, - nullptr, &p) == kal_err_not_supported, - "a grant is refused rather than a program started without it"); - } - std::printf("openkal-windows: what a started program receives\n"); return failures == 0 ? 0 : 1; }