From 0bacb83f0879573e990416b9242a75cebcb1acab Mon Sep 17 00:00:00 2001 From: Mike Saito Date: Sun, 12 Jul 2026 12:36:02 +0300 Subject: [PATCH 1/2] fix(vfs): defer host cursor commit in getdirentries Follow-up VFS hardening task applying the same guest-writes-first discipline established in the time subsystem to directory enumeration. Deferred Commit in KernelGetdirentriesCore: - Reordered guest output so the 512-byte dirent buffer is written first via TryWriteCompat, basep is updated second (when non-null) via TryWriteUInt64Compat, and directory.NextIndex is advanced only after both guest writes succeed. - Removed the early basep write at method entry that could mutate guest memory before buffer validation and advance the host cursor before a successful dirent delivery, causing permanent entry loss on MEMORY_FAULT at bufferAddress. - EOF handling: when currentIndex >= Entries.Length, write basep with the final offset and return 0 without mutating NextIndex, matching FreeBSD getdirentries(2) semantics and preventing infinite retry loops. KernelGetdents path: basePointerAddress is passed as 0, so the transaction collapses to buffer write then host cursor advance with no basep side effect. Out of scope: coalesced {id, size} writes in sceKernelAprResolveFilepathsToIdsAndFileSizes; NetCtl connected-state stubs. Files: KernelMemoryCompatExports.cs --- .../Kernel/KernelMemoryCompatExports.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 379b884e..e77a22b4 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -6350,19 +6350,19 @@ private static int KernelGetdirentriesCore(CpuContext ctx, int fd, ulong bufferA } var currentIndex = directory.NextIndex; - if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } - if (currentIndex >= directory.Entries.Length) { + if (basePointerAddress != 0 && + !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } var entryName = directory.Entries[currentIndex]; - directory.NextIndex = currentIndex + 1; var entryBytes = Encoding.UTF8.GetBytes(entryName); var nameLength = Math.Min(entryBytes.Length, 255); @@ -6381,6 +6381,13 @@ private static int KernelGetdirentriesCore(CpuContext ctx, int fd, ulong bufferA return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } + if (basePointerAddress != 0 && + !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + directory.NextIndex = currentIndex + 1; ctx[CpuRegister.Rax] = 512; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } From f3b0671de1a9f97455d9d1754dd178439613d33e Mon Sep 17 00:00:00 2001 From: Mike Saito Date: Sun, 12 Jul 2026 13:05:19 +0300 Subject: [PATCH 2/2] fix(vfs): resolve-first bulk commit in APR filepath resolution Refactored sceKernelAprResolveFilepathsToIdsAndFileSizes to stop writing ids and sizes into guest memory one element at a time. - Removed the uint.MaxValue placeholder write at the start of each loop iteration. - Path resolution and file size lookup now fill host-side buffers first; on EFAULT or NOT_FOUND the guest ids/sizes arrays are left untouched. - ids and sizes are packed into contiguous byte buffers and written with one TryWriteCompat call per output array instead of separate TryWriteUInt32Compat / TryWriteUInt64Compat per index. - AmprFileRegistry.Register is called only after guest writes succeed. - AmprFileRegistry.ComputeFileId is internal so ids can be computed without registering paths during the resolve loop. Files: KernelMemoryCompatExports.cs, AmprFileRegistry.cs --- src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs | 2 +- .../Kernel/KernelMemoryCompatExports.cs | 50 ++++++++++++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs index 084c8e64..d808b9a8 100644 --- a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs +++ b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs @@ -21,7 +21,7 @@ public static bool TryGetHostPath(uint id, out string hostPath) return _hostPathsById.TryGetValue(id, out hostPath!); } - private static uint ComputeFileId(string guestPath) + internal static uint ComputeFileId(string guestPath) { var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath); diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index e77a22b4..5f0e2ced 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -1689,15 +1689,15 @@ public static int KernelAprResolveFilepathsToIdsAndFileSizes(CpuContext ctx) return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + var entryCount = (int)count; + Span localIds = count <= 256 ? stackalloc uint[entryCount] : new uint[entryCount]; + Span localSizes = count <= 128 ? stackalloc ulong[entryCount] : new ulong[entryCount]; + var resolvedGuestPaths = new string[entryCount]; + var resolvedHostPaths = new string[entryCount]; + for (ulong i = 0; i < count; i++) { - if (idsAddress != 0 && - !TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue)) - { - KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } - + var index = (int)i; if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath)) { KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); @@ -1712,23 +1712,47 @@ public static int KernelAprResolveFilepathsToIdsAndFileSizes(CpuContext ctx) return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - var fileId = AmprFileRegistry.Register(guestPath, hostPath); + var fileId = AmprFileRegistry.ComputeFileId(guestPath); LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}"); - if (idsAddress != 0 && - !TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId)) + localIds[index] = fileId; + localSizes[index] = fileSize; + resolvedGuestPaths[index] = guestPath; + resolvedHostPaths[index] = hostPath; + } + + Span sizePayload = count <= 64 ? stackalloc byte[entryCount * sizeof(ulong)] : new byte[entryCount * sizeof(ulong)]; + for (ulong i = 0; i < count; i++) + { + BinaryPrimitives.WriteUInt64LittleEndian(sizePayload[(int)(i * sizeof(ulong))..], localSizes[(int)i]); + } + + if (!TryWriteCompat(ctx, sizesAddress, sizePayload)) + { + KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (idsAddress != 0) + { + Span idPayload = count <= 128 ? stackalloc byte[entryCount * sizeof(uint)] : new byte[entryCount * sizeof(uint)]; + for (ulong i = 0; i < count; i++) { - KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + BinaryPrimitives.WriteUInt32LittleEndian(idPayload[(int)(i * sizeof(uint))..], localIds[(int)i]); } - if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize)) + if (!TryWriteCompat(ctx, idsAddress, idPayload)) { KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } } + for (var i = 0; i < entryCount; i++) + { + AmprFileRegistry.Register(resolvedGuestPaths[i], resolvedHostPaths[i]); + } + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; }