[Kernel] Match path cache comparisons to host filesystem case sensitivity (#381)

The negative-stat cache and the apr file-size cache memoize host
filesystem probe outcomes, but both were keyed with an ignore-case
comparer while the probes themselves (File.Exists/Directory.Exists/
FileInfo) are case-sensitive on Linux. That aliases distinct paths:

- stat("/app0/DATA.BIN") fails, the miss is cached, and a later
  stat("/app0/Data.bin") is answered NOT_FOUND from the cache without
  ever probing the disk - even though the file exists and the probe
  would succeed.
- sceKernelAprResolveFilepathsToIdsAndFileSizes serves the cached size
  of a case-distinct sibling file instead of the file's own size.

The registered-mount containment guard had the inverse problem: the
ignore-case StartsWith accepted a ".." path that resolves into a
sibling directory differing from the mount root only by case
("…/Save" vs "…/save"), letting guest I/O escape the mount.

All three sites now compare with the host filesystem's semantics:
ordinal-ignore-case on Windows, ordinal elsewhere. Windows behavior is
unchanged. Tests probe actual host filesystem behavior with real temp
files and skip their case-specific sections on case-insensitive hosts.
This commit is contained in:
999sian
2026-07-18 11:36:01 +01:00
committed by GitHub
parent 13269797bf
commit f84d869795
2 changed files with 167 additions and 4 deletions
@@ -110,8 +110,18 @@ public static partial class KernelMemoryCompatExports
private static readonly Dictionary<ulong, string> _mappedRegionNames = new();
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
// Both caches memoize host filesystem probe outcomes, so their key
// equivalence must match the host filesystem's: Windows resolves names
// case-insensitively, but Linux hosts are case-sensitive, and an
// ignore-case cache there aliases distinct paths — a cached miss for
// "/app0/DATA.BIN" keeps answering NOT_FOUND for "/app0/Data.bin" even
// though that file exists and a fresh probe would find it.
private static readonly StringComparer HostFsPathComparer =
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
private static readonly StringComparison HostFsPathComparison =
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
private static readonly HashSet<string> _negativeStatCache = new(HostFsPathComparer);
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPathComparer);
private static long _nextFileDescriptor = 2;
internal static int AllocateGuestFileDescriptor()
@@ -4706,8 +4716,12 @@ public static partial class KernelMemoryCompatExports
matchedHostRoot,
NormalizeMountRelativePath(relativePath)));
var rootWithSeparator = Path.TrimEndingDirectorySeparator(matchedHostRoot) + Path.DirectorySeparatorChar;
if (!string.Equals(candidate, matchedHostRoot, StringComparison.OrdinalIgnoreCase) &&
!candidate.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase))
// Host-semantics comparison: an ignore-case check on a case-sensitive
// host would let a relative path escape into a sibling directory that
// differs from the mount root only by case (root ".../Save" vs
// sibling ".../save").
if (!string.Equals(candidate, matchedHostRoot, HostFsPathComparison) &&
!candidate.StartsWith(rootWithSeparator, HostFsPathComparison))
{
return false;
}