diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index d82b7ed..e721e3c 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -5662,6 +5662,74 @@ public static partial class KernelMemoryCompatExports return true; } + /// + /// Inserts into the tracked-region table, + /// carving it out of any regions it overlaps and preserving the parts of + /// those regions that fall outside it. + /// + /// + /// The table is a SortedList keyed by start address, so assigning directly + /// destroys any existing entry that happens to share a start address. That + /// silently discarded enclosing reservations: a title reserves a large range + /// and then commits a small mapping at the same base, and the record of + /// everything past the small mapping disappears — leaving sceKernelVirtualQuery + /// unable to find memory the guest legitimately owns. + /// + /// Carving also keeps the table non-overlapping. Previously a new region + /// starting inside an existing one produced two overlapping entries, which + /// the ordered scan in TryFindVirtualQueryRegionLocked is not written to + /// expect. + /// + private static void ReplaceMappedRegionRangeLocked(MappedRegion replacement) + { + if (replacement.Length == 0 || + !TryAddU64(replacement.Address, replacement.Length, out var replacementEnd)) + { + _mappedRegions[replacement.Address] = replacement; + return; + } + + var start = replacement.Address; + List? overlapping = null; + foreach (var region in _mappedRegions.Values) + { + if (region.Length == 0 || + !TryAddU64(region.Address, region.Length, out var regionEnd)) + { + continue; + } + + if (region.Address < replacementEnd && regionEnd > start) + { + (overlapping ??= []).Add(region); + } + } + + if (overlapping is not null) + { + foreach (var region in overlapping) + { + _mappedRegions.Remove(region.Address); + } + + foreach (var region in overlapping) + { + var regionEnd = region.Address + region.Length; + if (region.Address < start) + { + AddMappedRegionSliceLocked(region, region.Address, start, region.Protection); + } + + if (regionEnd > replacementEnd) + { + AddMappedRegionSliceLocked(region, replacementEnd, regionEnd, region.Protection); + } + } + } + + _mappedRegions[start] = replacement; + } + private static void AddMappedRegionSliceLocked( MappedRegion source, ulong start, diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 4785bb4..c7c297a 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -1133,6 +1133,90 @@ public static class KernelPthreadExtendedCompatExports LibraryName = "libKernel")] public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx); + [SysAbiExport( + Nid = "SFxTMOfuCkE", + ExportName = "pthread_rwlock_tryrdlock", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadRwlockTryrdlock(CpuContext ctx) => + PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: false); + + [SysAbiExport( + Nid = "XhWHn6P5R7U", + ExportName = "pthread_rwlock_trywrlock", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixPthreadRwlockTrywrlock(CpuContext ctx) => + PthreadRwlockTryLockCore(ctx, ctx[CpuRegister.Rdi], write: true); + + /// + /// Non-blocking counterpart of : acquires + /// only if the lock is free right now, otherwise reports BUSY. + /// + /// + /// Deliberately not routed through TryAcquireBlockedRwlock. That helper exists + /// for the scheduler resume path and decrements WaitingWriters on success, + /// which is correct only for a thread that previously incremented it. A fresh + /// try never did, so reusing it would silently consume another thread's + /// waiter count and let a queued writer be skipped. + /// + private static int PthreadRwlockTryLockCore(CpuContext ctx, ulong rwlockAddress, bool write) + { + if (rwlockAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); + lock (rwlock.SyncRoot) + { + if (write) + { + if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + + // Mirrors the blocking path's re-entrant compat-writer grant so the + // two agree on what counts as already owning the lock. + if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0) + { + rwlock.AddCompatWriter(currentThreadId); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (rwlock.WriterThreadId != 0 || + rwlock.ReaderTotalCount != 0 || + rwlock.CompatWriterTotalCount != 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + + DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "trywrlock"); + rwlock.WriterThreadId = currentThreadId; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (rwlock.WriterThreadId == currentThreadId) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + } + + if (ReaderMustWaitForRwlock(rwlock, currentThreadId)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + + rwlock.AddReader(currentThreadId); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + } + [SysAbiExport( Nid = "+L98PIbGttk", ExportName = "scePthreadRwlockUnlock", diff --git a/src/SharpEmu.Libs/Network/NetExports.cs b/src/SharpEmu.Libs/Network/NetExports.cs index 17f75fa..0751a32 100644 --- a/src/SharpEmu.Libs/Network/NetExports.cs +++ b/src/SharpEmu.Libs/Network/NetExports.cs @@ -184,6 +184,212 @@ public static class NetExports return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); } + /// + /// POSIX alias of ; identical + /// (fd, level, option, value, length) argument order. + /// + [SysAbiExport( + Nid = "fFxGkxF2bVo", + ExportName = "setsockopt", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixSetsockopt(CpuContext ctx) => NetSetsockopt(ctx); + + /// + /// Reads back the socket options this backend actually tracks: SO_NBIO, + /// SO_REUSEADDR and SO_ERROR. + /// + /// + /// Anything else returns EINVAL rather than a zero-filled buffer. A caller + /// that receives success for an option nobody stored would treat whatever + /// happens to be in its output buffer as the real setting, which is a harder + /// failure to trace than an explicit rejection. + /// + [SysAbiExport( + Nid = "6O8EwYOgH9Y", + ExportName = "getsockopt", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixGetsockopt(CpuContext ctx) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + var level = unchecked((int)ctx[CpuRegister.Rsi]); + var option = unchecked((int)ctx[CpuRegister.Rdx]); + var valueAddress = ctx[CpuRegister.Rcx]; + var lengthAddress = ctx[CpuRegister.R8]; + if (!_sockets.TryGetValue(id, out var socket)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + if (valueAddress == 0 || lengthAddress == 0 || level != 0xFFFF) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + Span lengthBytes = stackalloc byte[sizeof(int)]; + if (!ctx.Memory.TryRead(lengthAddress, lengthBytes)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + if (BinaryPrimitives.ReadInt32LittleEndian(lengthBytes) < sizeof(int)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + int value; + switch (option) + { + // ORBIS_NET_SO_NBIO: mirrors what sceNetSetsockopt stored. + case 0x1200: + value = socket.Blocking ? 0 : 1; + break; + case 0x0004: + value = (int)socket.GetSocketOption( + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress)! != 0 ? 1 : 0; + break; + // ORBIS_NET_SO_ERROR: nothing here records per-socket async errors, + // so report "no pending error" rather than inventing one. + case 0x1007: + value = 0; + break; + default: + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + Span valueBytes = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value); + BinaryPrimitives.WriteInt32LittleEndian(lengthBytes, sizeof(int)); + if (!ctx.Memory.TryWrite(valueAddress, valueBytes) || + !ctx.Memory.TryWrite(lengthAddress, lengthBytes)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + TraceNet("socket.getsockopt", id, unchecked((uint)option), unchecked((uint)value), 0); + return ctx.SetReturn(0); + } + + [SysAbiExport( + Nid = "fZOeZIOEmLw", + ExportName = "send", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixSend(CpuContext ctx) + { + var id = unchecked((int)ctx[CpuRegister.Rdi]); + var bufferAddress = ctx[CpuRegister.Rsi]; + var length = unchecked((int)ctx[CpuRegister.Rdx]); + if (!_sockets.TryGetValue(id, out var socket)) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + + if (length < 0 || (length != 0 && bufferAddress == 0)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + if (length == 0) + { + return ctx.SetReturn(0); + } + + var payload = new byte[length]; + if (!ctx.Memory.TryRead(bufferAddress, payload)) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + + try + { + var sent = socket.Send(payload, SocketFlags.None); + TraceNet("socket.send", id, unchecked((uint)length), unchecked((uint)sent), 0); + return ctx.SetReturn(sent); + } + catch (SocketException exception) + when (exception.SocketErrorCode == SocketError.WouldBlock) + { + return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock); + } + catch (SocketException) + { + return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument); + } + catch (ObjectDisposedException) + { + return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor); + } + } + + /// + /// Formats a binary address as text. Pure conversion with no socket state, + /// so it behaves identically to the console version for AF_INET/AF_INET6. + /// + [SysAbiExport( + Nid = "5jRCs2axtr4", + ExportName = "inet_ntop", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixInetNtop(CpuContext ctx) + { + var family = unchecked((int)ctx[CpuRegister.Rdi]); + var sourceAddress = ctx[CpuRegister.Rsi]; + var destinationAddress = ctx[CpuRegister.Rdx]; + var destinationSize = unchecked((int)ctx[CpuRegister.Rcx]); + if (sourceAddress == 0 || destinationAddress == 0 || destinationSize <= 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + // ORBIS_NET_AF_INET / ORBIS_NET_AF_INET6, matching TryMapAddressFamily. + var addressLength = family switch + { + 2 => 4, + 28 => 16, + _ => 0, + }; + + if (addressLength == 0) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var rawAddress = new byte[addressLength]; + if (!ctx.Memory.TryRead(sourceAddress, rawAddress)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var text = new IPAddress(rawAddress).ToString(); + var encoded = Encoding.ASCII.GetBytes(text); + + // POSIX requires the terminator to fit as well; a truncated address string + // is worse than a reported failure because the caller cannot detect it. + if (encoded.Length + 1 > destinationSize) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var buffer = new byte[encoded.Length + 1]; + encoded.CopyTo(buffer, 0); + if (!ctx.Memory.TryWrite(destinationAddress, buffer)) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + // inet_ntop returns the destination pointer on success. + ctx[CpuRegister.Rax] = destinationAddress; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "bErx49PgxyY", ExportName = "sceNetBind",