From bc3159699635553c271493317ec6a90a3cbaee38 Mon Sep 17 00:00:00 2001 From: themuffinator Date: Wed, 26 Aug 2026 16:56:06 +0100 Subject: [PATCH] filesystem: allow ellipses in explicit save paths --- docs/dev/release-completion.md | 1 + src/framework/FileSystem.cpp | 30 ++++++++++++++++-- tools/tests/filesystem_write_qpath_safety.py | 33 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/dev/release-completion.md b/docs/dev/release-completion.md index f263106c..2271166f 100644 --- a/docs/dev/release-completion.md +++ b/docs/dev/release-completion.md @@ -36,6 +36,7 @@ is `docs/dev/macos-moltenvk-decision.md`. ## Ready For Changelog +- [x] Linux save and configuration directories now work when an explicit home/save path contains an ordinary ellipsis (`...`) or another repeated-dot name. Directory creation still rejects a complete `..` parent component, preserving traversal protection without mistaking legitimate absolute paths for traversal attempts. This fixes the persistent-settings failure reported in GitHub issue #127. - [x] Milestone F now has a locally validated, default-off implementation for explicitly authored PBR materials, bounded OpenGL specular probes, and atomic OpenGL clustered decals. The 2026-08-23 current-source debug x64 evidence passed the static PBR/advanced-lighting checks, the final native 10/10 run, the final focused engine 2/2 rerun after an earlier 8/8 safe set, all four retail-PK4 compatibility roles, forced GL 3.3/4.1/4.3/4.5 PBR execution, the narrow validation-enabled Vulkan route, and exact `r_rendererModernQuality 0` rollback against leaf-disabled images. The stock baseline used all 40 retail PK4s with zero loose retail files; the generated PBR fixture remained temporary under `.tmp/`. PBR, probe, decal, and modern-visible controls stay default-off, `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` stays `0`, and this local implementation exit is not final committed-package/platform/driver promotion or broad authored probe/decal visual qualification. - [x] Milestone F's remaining quality leaves are implemented independently and default off on OpenGL and Vulkan. Bounded view-aligned froxel integration, depth-normal SSR, and fixed eight-tap depth-derived SSGI share the native scene-colour/depth presentation tail without allocating temporal history for effect-only use. The stable eight-float contract, final 11/11 native suite, static backend checks, Vulkan shader pin, combined GL/Vulkan `game/airdefense1` gameplay, individual OpenGL leaves, visible enabled delta, and exact master-off zero packet pass locally with engine screenshots and clean API/error counters. `r_rendererModernQuality 0` remains the one-setting rollback. This is a scoped screen-space implementation, not shadowed light-injected volumetrics, roughness-aware G-buffer reflections, world-space GI, or whole-frame modern-lighting promotion. - [x] The post-roadmap performance pass removes two default-path regressions without enabling the guarded level-load cache experiment. Cinematic fast-forward no longer samples non-presented poses for every spawned entity on every 60 Hz tick, visible-frame interpolation samples active movers plus their bounded physics-team members and cleanup members, and maps without baked light-grid assets no longer construct a 22,024-point bake/debug layout during ordinary loading. On `game/airdefense1`, the controlled 186-second scripted skip fell from 7,113 to 5,770 ms at the exact same game-time endpoint and missing-grid setup fell from 357 to 1 ms; the isolated end-to-end run improved from 33,121 to 25,091 ms and steady pacing from 110.8 to 120.4 Hz, with the documented cache/scene-variance qualification. The final staged build also passed default `game/airdefense2`, pure auto-joined `mp/q4dm1`, and a four-role compatibility run against 40 retail PK4s with zero loose retail files on the tested Windows system. diff --git a/src/framework/FileSystem.cpp b/src/framework/FileSystem.cpp index fdfe6f43..b445a32e 100644 --- a/src/framework/FileSystem.cpp +++ b/src/framework/FileSystem.cpp @@ -113,6 +113,33 @@ static bool FS_IsWindowsDeviceQPathSegment( const char *segment, int segmentLeng static_cast( segment[ 4 ] ) == 0xB3 ); } +/* +======================== +FS_HasParentOSPathSegment + +Explicit OS paths may legitimately contain repeated dots in a filename or +directory name. Reject only a complete parent-directory component so an +absolute save path cannot be used to make CreateOSPath back up the hierarchy. +======================== +*/ +static bool FS_HasParentOSPathSegment( const char *OSPath ) { + const char *segmentStart = OSPath; + for ( const char *scan = OSPath; ; scan++ ) { + const char c = *scan; + if ( c != '\0' && c != '/' && c != '\\' ) { + continue; + } + + if ( scan - segmentStart == 2 && segmentStart[ 0 ] == '.' && segmentStart[ 1 ] == '.' ) { + return true; + } + if ( c == '\0' ) { + return false; + } + segmentStart = scan + 1; + } +} + /* ======================== FS_ValidateRelativeWritePath @@ -2028,8 +2055,7 @@ void idFileSystemLocal::CreateOSPath( const char *OSPath ) { char *ofs; // make absolutely sure that it can't back up the path - // FIXME: what about c: ? - if ( strstr( OSPath, ".." ) || strstr( OSPath, "::" ) ) { + if ( FS_HasParentOSPathSegment( OSPath ) || strstr( OSPath, "::" ) ) { #ifdef _DEBUG common->DPrintf( "refusing to create relative path \"%s\"\n", OSPath ); #endif diff --git a/tools/tests/filesystem_write_qpath_safety.py b/tools/tests/filesystem_write_qpath_safety.py index b0599acb..22c78db2 100644 --- a/tools/tests/filesystem_write_qpath_safety.py +++ b/tools/tests/filesystem_write_qpath_safety.py @@ -68,6 +68,10 @@ def is_safe_relative_write_path_model(relative_path: str | None) -> bool: return True +def has_parent_os_path_segment_model(os_path: str) -> bool: + return any(segment == ".." for segment in os_path.replace("\\", "/").split("/")) + + def validate_behavior_model() -> None: accepted = ( "openq4.cfg", @@ -131,6 +135,26 @@ def validate_behavior_model() -> None: if is_safe_relative_write_path_model(path): raise AssertionError(f"Expected rejected relative mutation qpath: {path!r}") + accepted_os_paths = ( + "/home/player/openQ4.../baseoq4/openQ4Config.cfg", + "/mnt/games/build_(openQ4,_Linux,_GOG_Assets...)/userdata/player/openQ4Config.cfg", + r"C:\Games\openQ4...\baseoq4\openQ4Config.cfg", + r"C:\Games\version..candidate\baseoq4\openQ4Config.cfg", + ) + rejected_os_paths = ( + "../outside.cfg", + "/home/player/../outside.cfg", + r"C:\Games\..\outside.cfg", + r"\\server\share\folder\..\outside.cfg", + "/home/player/..", + ) + for path in accepted_os_paths: + if has_parent_os_path_segment_model(path): + raise AssertionError(f"Expected accepted explicit OS path: {path!r}") + for path in rejected_os_paths: + if not has_parent_os_path_segment_model(path): + raise AssertionError(f"Expected rejected explicit OS path: {path!r}") + def validate_generated_loadscreen_path_budget() -> None: final_qpath = "guis/assets/generated/loadscreens/airdefense1_1820x1024.tga" @@ -335,6 +359,7 @@ def validate_source_contract() -> None: header = read("src/framework/FileSystem.h") device_helper = function_body(source, "static bool FS_IsWindowsDeviceQPathSegment(") + parent_os_path_helper = function_body(source, "static bool FS_HasParentOSPathSegment(") validator = function_body(source, "static bool FS_ValidateRelativeWritePath(") open_write = function_body(source, "idFile *idFileSystemLocal::OpenFileWrite(") open_append = function_body(source, "idFile *idFileSystemLocal::OpenFileAppend(") @@ -367,6 +392,14 @@ def validate_source_contract() -> None: require(device_helper, "digit == 0xB9", "superscript Windows device-name rejection") require(device_helper, "digit == 0xC2", "UTF-8 superscript Windows device-name rejection") + require(parent_os_path_helper, "c != '/' && c != '\\\\'", "OS-path separator recognition") + require(parent_os_path_helper, "scan - segmentStart == 2", "exact parent-component length") + require(parent_os_path_helper, "segmentStart[ 0 ] == '.'", "parent-component first dot") + require(parent_os_path_helper, "segmentStart[ 1 ] == '.'", "parent-component second dot") + require(create_os_path, "FS_HasParentOSPathSegment( OSPath )", "explicit OS-path traversal rejection") + require(create_os_path, 'strstr( OSPath, "::" )', "legacy parent-path syntax rejection") + reject(create_os_path, 'strstr( OSPath, ".." )', "ordinary repeated dots remain valid") + for body, context, first_mutation in ( (write_file, "whole-file write API", "idFileSystemLocal::OpenFileWrite("), (open_write, "relative write API", "BuildOSPath("),