From 3540d676733efaddecf504b30e9a596465bd43f8 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Mon, 30 Mar 2026 15:07:08 +0200 Subject: Request validation and resilience improvements (#864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Security: Input validation & path safety - **Reject local file references by default** in package parsing — only allow when explicitly opted in by the service (`ParseFlags::kAllowLocalReferences`) and validated by an `ILocalRefPolicy` (fail-closed: no policy = rejected) - **`DataRootLocalRefPolicy`** restricts local ref paths to the server's data root via canonical path prefix matching - **Validate attachment hashes** in compute HTTP handlers — decompresses and re-hashes each attachment at ingestion time to reject tampered payloads - **Path traversal validation** for worker descriptions (`pathvalidation.h`) — rejects absolute paths, `..` components, Windows reserved device names, and invalid filename characters - **Harden CbPackage parsing** against corrupt inputs — overflow-safe attachment count, bounds checks on local ref offset/size, graceful failure instead of `ZEN_ASSERT` for untrusted data - **Harden legacy package parser** — reject zero-size binary fields, missing mappers, and optionally validate resolved attachment hashes - **Bounds check in `CbPackageReader::MarshalLocalChunkReference`** — detect when `MakeFromFile` silently clamps offset+size to file size ### Reliability: Lock consolidation & bug fixes - **Consolidate three action map locks into one** (`m_ActionMapLock`) — eliminates deadlock risk from multi-lock ordering, simplifies state transitions, and fixes a race where newly enqueued actions were briefly invisible to `GetActionResult`/`FindActionResult` - **Fix infinite loop in `BaseRunnerGroup::SubmitActions`** when actions exceed total runner capacity — cap round-robin at `TotalCapacity` and default unassigned results to "No capacity" - **Fix `MakeSafeAbsolutePathInPlace` for UNC paths** — `\server\share` now correctly becomes `\?\UNC\server\share` instead of `\?\server\share` - **Fix `max_retries=0`** — previously fell through to the default of 3; now correctly means "no retries" ### New: ManagedProcessRunner - Cross-platform process runner backed by `SubprocessManager` — uses async exit callbacks instead of polling, delegates CPU/memory metrics to the manager's built-in sampler - `ProcessGroup` (JobObject on Windows, process group on POSIX) for bulk cancellation on shutdown - `--managed` flag on `zen exec inproc` to select this runner - Refactored monitor thread lifecycle — `StartMonitorThread()` now called from derived constructors to avoid calling virtual functions from base constructor ### Process management - **Suppress crash dialogs** via `JOB_OBJECT_UILIMIT_ERRORMODE` + `SEM_NOGPFAULTERRORBOX` in both `WindowsProcessRunner` and `JobObject::Initialize` — prevents WER/Dr. Watson modal dialogs from blocking the monitor thread - **CREATE_SUSPENDED → AssignProcessToJobObject → ResumeThread** pattern in `WindowsProcessRunner` — ensures job object assignment before process execution - **Move stdout/stderr callbacks to `Spawn()` parameters** in `SubprocessManager` — prevents race where early output could be missed before callback installation - Consistent PID logging across all runner types ### Test infrastructure - **`zentest-appstub`**: Added `Fail` (configurable exit code) and `Crash` (abort / nullptr deref) test functions - **Compute integration tests**: exit code handling, auto-retry exhaustion, manual reschedule after failure, mixed success/failure queues, crash handling (abort + nullptr), crash auto-retry, immediate query visibility after enqueue - **Package format tests**: truncated header, bad magic, attachment count overflow, truncated data, local ref rejection/acceptance, policy enforcement (inside/outside root, traversal, no-policy fail-closed) - **Legacy package parser tests**: empty input, zero-size binary, hash resolution with/without mapper, hash mismatch detection - **UNC path tests** for `MakeSafeAbsolutePath` ### Misc - ANSI color helper macros (`ZEN_RED`, `ZEN_BRIGHT_WHITE`, etc.) and `ZEN_BOLD`/`ZEN_DIM`/etc. - Generic `fmt::formatter` for types with free `ToString` functions - Compute dashboard: truncated hash display with monospace font and hover for full value - Renamed `usonpackage_forcelink` → `cbpackage_forcelink` - Compute enabled by default in xmake config (releases still explicitly disable) --- src/zencore/filesystem.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 5 deletions(-) (limited to 'src/zencore/filesystem.cpp') diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index 416312cae..a63594be9 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -3277,12 +3277,23 @@ MakeSafeAbsolutePathInPlace(std::filesystem::path& Path) { Path = std::filesystem::absolute(Path).make_preferred(); #if ZEN_PLATFORM_WINDOWS - const std::string_view Prefix = "\\\\?\\"; - const std::u8string PrefixU8(Prefix.begin(), Prefix.end()); - std::u8string PathString = Path.u8string(); - if (!PathString.empty() && !PathString.starts_with(PrefixU8)) + const std::u8string_view LongPathPrefix = u8"\\\\?\\"; + const std::u8string_view UncPrefix = u8"\\\\"; + const std::u8string_view LongPathUncPrefix = u8"\\\\?\\UNC\\"; + + std::u8string PathString = Path.u8string(); + if (!PathString.empty() && !PathString.starts_with(LongPathPrefix)) { - PathString.insert(0, PrefixU8); + if (PathString.starts_with(UncPrefix)) + { + // UNC path: \\server\share → \\?\UNC\server\share + PathString.replace(0, UncPrefix.size(), LongPathUncPrefix); + } + else + { + // Local path: C:\foo → \\?\C:\foo + PathString.insert(0, LongPathPrefix); + } Path = PathString; } #endif // ZEN_PLATFORM_WINDOWS @@ -4049,6 +4060,54 @@ TEST_CASE("SharedMemory") CHECK(!OpenSharedMemory("SharedMemoryTest0", 482, false)); } +TEST_CASE("filesystem.MakeSafeAbsolutePath") +{ +# if ZEN_PLATFORM_WINDOWS + // Local path gets \\?\ prefix + { + std::filesystem::path Local = MakeSafeAbsolutePath("C:\\Users\\test"); + CHECK(Local.u8string().starts_with(u8"\\\\?\\")); + CHECK(Local.u8string().find(u8"C:\\Users\\test") != std::u8string::npos); + } + + // UNC path gets \\?\UNC\ prefix + { + std::filesystem::path Unc = MakeSafeAbsolutePath("\\\\server\\share\\path"); + std::u8string UncStr = Unc.u8string(); + CHECK_MESSAGE(UncStr.starts_with(u8"\\\\?\\UNC\\"), fmt::format("Expected \\\\?\\UNC\\ prefix, got '{}'", Unc)); + CHECK_MESSAGE(UncStr.find(u8"server\\share\\path") != std::u8string::npos, + fmt::format("Expected server\\share\\path in '{}'", Unc)); + // Must NOT produce \\?\\\server (double backslash after \\?\) + CHECK_MESSAGE(UncStr.find(u8"\\\\?\\\\\\") == std::u8string::npos, + fmt::format("Path contains invalid double-backslash after prefix: '{}'", Unc)); + } + + // Already-prefixed path is not double-prefixed + { + std::filesystem::path Already = MakeSafeAbsolutePath("\\\\?\\C:\\already\\prefixed"); + size_t Count = 0; + std::u8string Str = Already.u8string(); + for (size_t Pos = Str.find(u8"\\\\?\\"); Pos != std::u8string::npos; Pos = Str.find(u8"\\\\?\\", Pos + 1)) + { + ++Count; + } + CHECK_EQ(Count, 1); + } + + // Already-prefixed UNC path is not double-prefixed + { + std::filesystem::path AlreadyUnc = MakeSafeAbsolutePath("\\\\?\\UNC\\server\\share"); + size_t Count = 0; + std::u8string Str = AlreadyUnc.u8string(); + for (size_t Pos = Str.find(u8"\\\\?\\"); Pos != std::u8string::npos; Pos = Str.find(u8"\\\\?\\", Pos + 1)) + { + ++Count; + } + CHECK_EQ(Count, 1); + } +# endif // ZEN_PLATFORM_WINDOWS +} + TEST_SUITE_END(); #endif -- cgit v1.2.3