mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-18 15:31:31 +08:00
[Kernel] Implement the POSIX libKernel exports titles link directly
libKernel exports many routines under both sce-prefixed and plain POSIX NIDs, and shipped middleware links the latter. These nine are imported by DOOM + DOOM II (PPSA21444) and had no registration at all. Aliases onto existing implementations, identical argument order: mprotect (YQOfxL4QfeU), munmap (UqDGjXA5yUM), setsockopt (fFxGkxF2bVo) New: getpagesize reports OrbisPageSize (16 KiB), not the host 4 KiB. An allocator rounding to the host value produces sub-page offsets that every mapping call here rejects for misalignment. pthread_rwlock_tryrdlock/trywrlock get a dedicated non-blocking core. They deliberately do not reuse TryAcquireBlockedRwlock, which decrements WaitingWriters -- correct only for a thread that previously incremented it. A fresh try never did, so reusing it would consume another thread's waiter count and let a queued writer be skipped. getsockopt reads back the three options this backend tracks (SO_NBIO, SO_REUSEADDR, SO_ERROR) and rejects the rest rather than returning success with an untouched buffer the caller would treat as real. send maps WouldBlock onto the existing net error path. inet_ntop converts AF_INET/AF_INET6 and returns the destination pointer per POSIX, failing rather than truncating when it will not fit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5662,6 +5662,74 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inserts <paramref name="replacement"/> into the tracked-region table,
|
||||||
|
/// carving it out of any regions it overlaps and preserving the parts of
|
||||||
|
/// those regions that fall outside it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
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<MappedRegion>? 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(
|
private static void AddMappedRegionSliceLocked(
|
||||||
MappedRegion source,
|
MappedRegion source,
|
||||||
ulong start,
|
ulong start,
|
||||||
|
|||||||
@@ -1133,6 +1133,90 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockWrlock(ctx);
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Non-blocking counterpart of <see cref="PthreadRwlockLockCore"/>: acquires
|
||||||
|
/// only if the lock is free right now, otherwise reports BUSY.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
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(
|
[SysAbiExport(
|
||||||
Nid = "+L98PIbGttk",
|
Nid = "+L98PIbGttk",
|
||||||
ExportName = "scePthreadRwlockUnlock",
|
ExportName = "scePthreadRwlockUnlock",
|
||||||
|
|||||||
@@ -184,6 +184,212 @@ public static class NetExports
|
|||||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POSIX alias of <see cref="NetSetsockopt"/>; identical
|
||||||
|
/// (fd, level, option, value, length) argument order.
|
||||||
|
/// </summary>
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fFxGkxF2bVo",
|
||||||
|
ExportName = "setsockopt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSetsockopt(CpuContext ctx) => NetSetsockopt(ctx);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads back the socket options this backend actually tracks: SO_NBIO,
|
||||||
|
/// SO_REUSEADDR and SO_ERROR.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
[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<byte> 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<byte> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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(
|
[SysAbiExport(
|
||||||
Nid = "bErx49PgxyY",
|
Nid = "bErx49PgxyY",
|
||||||
ExportName = "sceNetBind",
|
ExportName = "sceNetBind",
|
||||||
|
|||||||
Reference in New Issue
Block a user