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",