From 956da769a3d7dbdf7335d6e26ecf9a66237fcbdd Mon Sep 17 00:00:00 2001 From: Radu Ursache <3800336+rursache@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:32:56 +0300 Subject: [PATCH] fix(kernel): finish Posix -1/errno for file ops and open EACCES (#567) Map UnauthorizedAccess on open to PERMISSION_DENIED and route Posix lseek/pread/pwrite/rename/etc failures through PosixFailure so libc-style callers get RAX=-1 plus TLS errno, matching open/read/write. --- .../Kernel/KernelFileExtendedExports.cs | 74 +++++++++-- .../Kernel/KernelMemoryCompatExports.cs | 5 +- .../Kernel/KernelMemoryCompatExportsTests.cs | 119 ++++++++++++++++++ 3 files changed, 185 insertions(+), 13 deletions(-) diff --git a/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs b/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs index bbd44c30..2aeb7f51 100644 --- a/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs @@ -46,7 +46,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixPread(CpuContext ctx) => KernelPreadCore(ctx); + public static int PosixPread(CpuContext ctx) + { + var result = KernelPreadCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -97,7 +103,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixPwrite(CpuContext ctx) => KernelPwriteCore(ctx); + public static int PosixPwrite(CpuContext ctx) + { + var result = KernelPwriteCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -149,7 +161,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFsync(CpuContext ctx) => KernelFsyncCore(ctx); + public static int PosixFsync(CpuContext ctx) + { + var result = KernelFsyncCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -157,7 +175,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFdatasync(CpuContext ctx) => KernelFsyncCore(ctx); + public static int PosixFdatasync(CpuContext ctx) + { + var result = KernelFsyncCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -210,7 +234,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx); + public static int PosixFtruncate(CpuContext ctx) + { + var result = KernelFtruncateCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -246,7 +276,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixTruncate(CpuContext ctx) => KernelTruncateCore(ctx); + public static int PosixTruncate(CpuContext ctx) + { + var result = KernelTruncateCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result); + } [SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -294,7 +330,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixRename(CpuContext ctx) => KernelRenameCore(ctx); + public static int PosixRename(CpuContext ctx) + { + var result = KernelRenameCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result); + } [SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -363,7 +405,10 @@ public static partial class KernelMemoryCompatExports { if (!_openFiles.TryGetValue(fd, out var stream)) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + return PosixFailure( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, + notFoundErrno: Ebadf); } // POSIX dup shares the open file description (and offset), which is @@ -386,7 +431,10 @@ public static partial class KernelMemoryCompatExports { if (!_openFiles.TryGetValue(oldFd, out var stream)) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + return PosixFailure( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, + notFoundErrno: Ebadf); } if (oldFd == newFd) @@ -412,7 +460,13 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFcntl(CpuContext ctx) => KernelFcntlCore(ctx); + public static int PosixFcntl(CpuContext ctx) + { + var result = KernelFcntlCore(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_OK + ? 0 + : PosixFailure(ctx, result, notFoundErrno: Ebadf); + } [SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 2b1461d6..3e62c202 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -1558,7 +1558,7 @@ public static partial class KernelMemoryCompatExports { LogOpenTrace($"_open fail path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} ex={ex.GetType().Name}: {ex.Message}"); return ex is UnauthorizedAccessException - ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT + ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } } @@ -2228,8 +2228,7 @@ public static partial class KernelMemoryCompatExports if (result != OrbisGen2Result.ORBIS_GEN2_OK) { - ctx[CpuRegister.Rax] = ulong.MaxValue; - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + return PosixFailure(ctx, (int)result, notFoundErrno: Ebadf); } ctx[CpuRegister.Rax] = unchecked((ulong)position); diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs index f1662443..a6ba24e5 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs @@ -125,6 +125,125 @@ public sealed class KernelMemoryCompatExportsTests Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); } + // FreeBSD/PS4 EBADF; PosixFailure maps ORBIS NOT_FOUND on fd calls to this. + private const int Ebadf = 9; + // FreeBSD/PS4 EACCES; PosixFailure maps ORBIS PERMISSION_DENIED to this. + private const int Eacces = 13; + // TLS slot used by KernelRuntimeCompatExports.TrySetErrno (FsBase + 0x40). + private const ulong TlsErrnoOffset = 0x40; + + [Fact] + public void PosixLseek_BadDescriptorReturnsMinusOneWithEbadf() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong fsBase = memoryBase + 0x100; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5) + { + FsBase = fsBase, + }; + context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd + context[CpuRegister.Rsi] = 0; + context[CpuRegister.Rdx] = 0; // SEEK_SET + + var result = KernelMemoryCompatExports.PosixLseek(context); + + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + Assert.Equal(Ebadf, ReadErrno(memory, fsBase)); + } + + [Fact] + public void PosixPread_BadDescriptorReturnsMinusOneWithEbadf() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong bufferAddress = memoryBase + 0x200; + const ulong fsBase = memoryBase + 0x100; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5) + { + FsBase = fsBase, + }; + context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd + context[CpuRegister.Rsi] = bufferAddress; + context[CpuRegister.Rdx] = 0x40; + context[CpuRegister.Rcx] = 0; // offset + + var result = KernelMemoryCompatExports.PosixPread(context); + + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + Assert.Equal(Ebadf, ReadErrno(memory, fsBase)); + } + + [Fact] + public void KernelPread_BadDescriptorStillReturnsOrbisNotFound() + { + // sceKernel* entry points keep the raw Orbis ABI; only Posix* maps to -1/errno. + const ulong memoryBase = 0x1_0000_0000; + const ulong bufferAddress = memoryBase + 0x200; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0x80020002; + context[CpuRegister.Rsi] = bufferAddress; + context[CpuRegister.Rdx] = 0x40; + context[CpuRegister.Rcx] = 0; + + var result = KernelMemoryCompatExports.KernelPread(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result); + } + + [Fact] + public void PosixOpen_MutatingApp0ReturnsMinusOneWithEacces() + { + // /app0 is read-only for mutating opens (retail semantics). That path + // returns PERMISSION_DENIED which PosixFailure maps to EACCES - the same + // errno UnauthorizedAccessException open failures now produce. + var tempRoot = Path.Combine( + Path.GetTempPath(), + $"sharpemu-posix-open-eacces-{Guid.NewGuid():N}"); + var app0Root = Path.Combine(tempRoot, "app0"); + Directory.CreateDirectory(app0Root); + KernelMemoryCompatExports.RegisterGuestPathMount("/app0", app0Root); + + try + { + const ulong memoryBase = 0x1_0000_0000; + const ulong pathAddress = memoryBase + 0x200; + const ulong fsBase = memoryBase + 0x100; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5) + { + FsBase = fsBase, + }; + memory.WriteCString(pathAddress, "/app0/readonly-create.bin"); + context[CpuRegister.Rdi] = pathAddress; + context[CpuRegister.Rsi] = 0x0201; // O_WRONLY | O_CREAT + + var result = KernelMemoryCompatExports.PosixOpen(context); + + Assert.Equal(-1, result); + Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); + Assert.Equal(Eacces, ReadErrno(memory, fsBase)); + } + finally + { + KernelMemoryCompatExports.UnregisterGuestPathMount("/app0"); + if (Directory.Exists(tempRoot)) + { + Directory.Delete(tempRoot, recursive: true); + } + } + } + + private static int ReadErrno(FakeCpuMemory memory, ulong fsBase) + { + Span bytes = stackalloc byte[sizeof(int)]; + Assert.True(memory.TryRead(fsBase + TlsErrnoOffset, bytes)); + return BitConverter.ToInt32(bytes); + } + [Fact] public void Sprintf_ReadsVariadicDoubleFromXmmRegister() {