[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 <app>/binaries/<platform>, 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.
This commit is contained in:
Youss
2026-07-19 19:34:00 +02:00
committed by GitHub
parent 3ebfc56d4c
commit 9ff60abb9b
@@ -1,4 +1,4 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
@@ -4729,7 +4729,7 @@ public static partial class KernelMemoryCompatExports
!guestPath.StartsWith("/", StringComparison.Ordinal) && !guestPath.StartsWith("/", StringComparison.Ordinal) &&
!guestPath.StartsWith("\\", StringComparison.Ordinal)) !guestPath.StartsWith("\\", StringComparison.Ordinal))
{ {
var relative = guestPath.Replace('/', Path.DirectorySeparatorChar); var relative = NormalizeMountRelativePath(guestPath);
return Path.Combine(app0Root, relative); return Path.Combine(app0Root, relative);
} }
} }
@@ -4804,12 +4804,40 @@ public static partial class KernelMemoryCompatExports
return _cachedApp0Root; 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
// <app>/binaries/<platform>, 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) private static string NormalizeMountRelativePath(string relativePath)
{ {
return relativePath var segments = relativePath.Split(
.TrimStart('/', '\\') new[] { '/', '\\' },
.Replace('/', Path.DirectorySeparatorChar) StringSplitOptions.RemoveEmptyEntries);
.Replace('\\', Path.DirectorySeparatorChar); var resolved = new List<string>(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() private static string ResolveDevlogAppRoot()