From 9ff60abb9b9a94e07b6b69d99e87e191092555a9 Mon Sep 17 00:00:00 2001 From: Youss <156013413+ftre120@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:34:00 +0200 Subject: [PATCH] [Kernel] Clamp guest path traversal at the mount root (#437) NormalizeMountRelativePath only stripped leading separators and swapped slashes; it never resolved "." or ".." segments. The bare-relative fallback in ResolveGuestPath did not even call it -- it combined the guest path against the app0 root verbatim. A guest path containing ".." therefore escaped its mount into the host filesystem. Unreal Engine titles hit this constantly: their base directory is /binaries/, so they address content with "../../../" prefixes that resolve back inside /app0 on real hardware. Here those walked out of the game folder entirely -- The Invincible (PPSA06426) opened "/app0/.." and enumerated the host's Downloads directory, listing unrelated user files, and never located its own content tree. Resolve "." and ".." while walking the segments and clamp the result at the mount root, then route the bare-relative fallback through the same normalizer. This both closes the sandbox escape and makes the engine's relative content paths land where the title expects. Verified on The Invincible: no resolved host path contains ".." any more, and the title now enumerates its own content directories (content/paks, content/movies, content/locale/*) instead of an unrelated host folder. No regression on Dead Cells (PPSA15552): still reaches AGC rendering and presents frames with zero mutex errors. --- .../Kernel/KernelMemoryCompatExports.cs | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 467ae4e4..f4ff293a 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2026 SharpEmu Emulator Project +// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; @@ -4729,7 +4729,7 @@ public static partial class KernelMemoryCompatExports !guestPath.StartsWith("/", StringComparison.Ordinal) && !guestPath.StartsWith("\\", StringComparison.Ordinal)) { - var relative = guestPath.Replace('/', Path.DirectorySeparatorChar); + var relative = NormalizeMountRelativePath(guestPath); return Path.Combine(app0Root, relative); } } @@ -4804,12 +4804,40 @@ public static partial class KernelMemoryCompatExports return _cachedApp0Root; } + // Resolves "." and ".." inside a mount-relative guest path and clamps the + // result at the mount root, so a guest path can never escape into the host + // filesystem. Unreal Engine titles depend on this: their base directory is + // /binaries/, so they address content with "../../../" + // prefixes that land back inside /app0 on real hardware. Combining those + // raw against the app0 root walked out of the game folder entirely, so the + // title enumerated an unrelated host directory and never found its .pak files. private static string NormalizeMountRelativePath(string relativePath) { - return relativePath - .TrimStart('/', '\\') - .Replace('/', Path.DirectorySeparatorChar) - .Replace('\\', Path.DirectorySeparatorChar); + var segments = relativePath.Split( + new[] { '/', '\\' }, + StringSplitOptions.RemoveEmptyEntries); + var resolved = new List(segments.Length); + foreach (var segment in segments) + { + if (segment == ".") + { + continue; + } + + if (segment == "..") + { + if (resolved.Count > 0) + { + resolved.RemoveAt(resolved.Count - 1); + } + + continue; + } + + resolved.Add(segment); + } + + return string.Join(Path.DirectorySeparatorChar, resolved); } private static string ResolveDevlogAppRoot()