Files
sharpemu/src/SharpEmu.Libs/Network/NetExports.cs
T
can a8be1daca4 Fix sceNetHtonl/Htons/Ntohl/Ntohs discarding their converted result (#211)
Each of the four byte-order helpers computed the swap into Rax and then
returned via ctx.SetReturn(0). SetReturn writes its argument into Rax, so it
immediately overwrote the converted value with 0 and the guest saw every
sceNetHtonl / sceNetHtons / sceNetNtohl / sceNetNtohs call return 0.

Leave the swapped value in Rax and return ORBIS_GEN2_OK as the dispatch status
instead, matching how the value-returning exports in this file (e.g.
sceNetPoolCreate) already work.

Add a NetExports test suite covering the swaps, the 16-bit width masking, an
htonl/ntohl round-trip, and a regression guard that a non-zero input never
converts to 0.
2026-07-16 16:24:34 +03:00

563 lines
20 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Network;
public static class NetExports
{
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
private const int NetErrnoBadFileDescriptor = 9;
private const int NetErrnoInvalidArgument = 22;
private const int NetErrnoWouldBlock = 35;
private const int NetErrnoAddressInUse = 48;
private const int NetErrnoNotInitialized = 200;
private const int MaxNameLength = 256;
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static int _nextSocketId = 0x4000;
// The platform networking module is usable immediately after it is loaded.
// Games and middleware (notably FMOD) can create internal sockets before an
// explicit sceNetInit call reaches application code.
private static bool _initialized = true;
[ThreadStatic]
private static nint _errnoAddress;
private sealed record NetPool(string Name, int Size, int Flags);
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
[SysAbiExport(
Nid = "Nlev7Lg8k3A",
ExportName = "sceNetInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetInit(CpuContext ctx)
{
_initialized = true;
TraceNet("init", 0, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "cTGkc6-TBlI",
ExportName = "sceNetTerm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetTerm(CpuContext ctx)
{
_initialized = false;
_pools.Clear();
_resolvers.Clear();
foreach (var socket in _sockets.Values)
{
socket.Dispose();
}
_sockets.Clear();
TraceNet("term", 0, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "Q4qBuN-c0ZM",
ExportName = "sceNetSocket",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSocket(CpuContext ctx)
{
if (!_initialized)
{
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
}
var nameAddress = ctx[CpuRegister.Rdi];
var family = unchecked((int)ctx[CpuRegister.Rsi]);
var type = unchecked((int)ctx[CpuRegister.Rdx]);
var protocol = unchecked((int)ctx[CpuRegister.Rcx]);
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
if (!TryTranslateSocketParameters(family, type, protocol, out var addressFamily, out var socketType, out var protocolType))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
try
{
var socket = new Socket(addressFamily, socketType, protocolType);
var id = Interlocked.Increment(ref _nextSocketId);
_sockets[id] = socket;
TraceNet("socket.create", id, unchecked((ulong)family), unchecked((ulong)type), unchecked((ulong)protocol));
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "45ggEzakPJQ",
ExportName = "sceNetSocketClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSocketClose(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryRemove(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
socket.Dispose();
TraceNet("socket.close", id, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "2mKX2Spso7I",
ExportName = "sceNetSetsockopt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetSetsockopt(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 valueLength = unchecked((int)ctx[CpuRegister.R8]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
// ORBIS_NET_SOL_SOCKET / ORBIS_NET_SO_NBIO. This is the first option
// used by FMOD's discovery socket and maps directly to host blocking.
if (level == 0xFFFF && option == 0x1200)
{
Span<byte> value = stackalloc byte[sizeof(int)];
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
socket.Blocking = BinaryPrimitives.ReadInt32LittleEndian(value) == 0;
TraceNet("socket.nonblocking", id, socket.Blocking ? 0UL : 1UL, 0, 0);
return ctx.SetReturn(0);
}
// ORBIS_NET_SO_REUSEADDR uses the BSD value 0x0004.
if (level == 0xFFFF && option == 0x0004)
{
Span<byte> value = stackalloc byte[sizeof(int)];
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
socket.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress,
BinaryPrimitives.ReadInt32LittleEndian(value) != 0);
TraceNet("socket.reuseaddr", id, BinaryPrimitives.ReadUInt32LittleEndian(value), 0, 0);
return ctx.SetReturn(0);
}
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
[SysAbiExport(
Nid = "bErx49PgxyY",
ExportName = "sceNetBind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetBind(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
if (!TryReadSocketAddress(ctx, ctx[CpuRegister.Rsi], unchecked((int)ctx[CpuRegister.Rdx]), out var endpoint))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
try
{
socket.Bind(endpoint);
TraceNet("socket.bind", id, unchecked((ulong)endpoint.Port), 0, 0);
return ctx.SetReturn(0);
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
return SetNetError(ctx, NetErrorAddressInUse, NetErrnoAddressInUse);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "kOj1HiAGE54",
ExportName = "sceNetListen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetListen(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
try
{
socket.Listen(Math.Max(0, unchecked((int)ctx[CpuRegister.Rsi])));
TraceNet("socket.listen", id, ctx[CpuRegister.Rsi], 0, 0);
return ctx.SetReturn(0);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "PIWqhn9oSxc",
ExportName = "sceNetAccept",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetAccept(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_sockets.TryGetValue(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
try
{
var accepted = socket.Accept();
var acceptedId = Interlocked.Increment(ref _nextSocketId);
_sockets[acceptedId] = accepted;
TraceNet("socket.accept", acceptedId, unchecked((ulong)id), 0, 0);
ctx[CpuRegister.Rax] = unchecked((ulong)acceptedId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.WouldBlock or SocketError.IOPending)
{
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
}
[SysAbiExport(
Nid = "HQOwnfMGipQ",
ExportName = "sceNetErrnoLoc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetErrnoLoc(CpuContext ctx)
{
if (_errnoAddress == 0)
{
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
Marshal.WriteInt32(_errnoAddress, 0);
}
ctx[CpuRegister.Rax] = unchecked((ulong)_errnoAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "dgJBaeJnGpo",
ExportName = "sceNetPoolCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetPoolCreate(CpuContext ctx)
{
var nameAddress = ctx[CpuRegister.Rdi];
var size = unchecked((int)ctx[CpuRegister.Rsi]);
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (size <= 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
var id = Interlocked.Increment(ref _nextPoolId);
_pools[id] = new NetPool(name, size, flags);
TraceNet("pool.create", id, unchecked((ulong)size), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "K7RlrTkI-mw",
ExportName = "sceNetPoolDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetPoolDestroy(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_pools.TryRemove(id, out _))
{
return ctx.SetReturn(NetErrorBadFileDescriptor);
}
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "9T2pDF2Ryqg",
ExportName = "sceNetHtonl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtonl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
// The byte-swapped result is the return value and already lives in Rax; return OK as the
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "iWQWrwiSt8A",
ExportName = "sceNetHtons",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtons(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
// The byte-swapped result is the return value and already lives in Rax; return OK as the
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "pQGpHYopAIY",
ExportName = "sceNetNtohl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
// The byte-swapped result is the return value and already lives in Rax; return OK as the
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Rbvt+5Y2iEw",
ExportName = "sceNetNtohs",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohs(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
// The byte-swapped result is the return value and already lives in Rax; return OK as the
// dispatch status without going through SetReturn, which would overwrite Rax with 0.
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverCreate(CpuContext ctx)
{
var nameAddress = ctx[CpuRegister.Rdi];
var poolId = unchecked((int)ctx[CpuRegister.Rsi]);
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (flags != 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
var id = Interlocked.Increment(ref _nextResolverId);
_resolvers[id] = new ResolverContext(name, poolId, flags, 0);
TraceNet("resolver.create", id, unchecked((ulong)poolId), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kJlYH5uMAWI",
ExportName = "sceNetResolverDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverDestroy(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
return _resolvers.TryRemove(id, out _)
? ctx.SetReturn(0)
: ctx.SetReturn(NetErrorBadFileDescriptor);
}
[SysAbiExport(
Nid = "J5i3hiLJMPk",
ExportName = "sceNetResolverGetError",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverGetError(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
}
if (!_resolvers.TryGetValue(id, out var resolver))
{
return ctx.SetReturn(NetErrorBadFileDescriptor);
}
Span<byte> status = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
return ctx.Memory.TryWrite(statusAddress, status)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetNetError(CpuContext ctx, int result, int errno)
{
if (_errnoAddress == 0)
{
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
}
Marshal.WriteInt32(_errnoAddress, errno);
return ctx.SetReturn(result);
}
private static bool TryTranslateSocketParameters(
int family,
int type,
int protocol,
out AddressFamily addressFamily,
out SocketType socketType,
out ProtocolType protocolType)
{
addressFamily = family switch
{
2 => AddressFamily.InterNetwork,
28 => AddressFamily.InterNetworkV6,
_ => AddressFamily.Unspecified,
};
socketType = type switch
{
1 => SocketType.Stream,
2 => SocketType.Dgram,
_ => SocketType.Unknown,
};
protocolType = protocol switch
{
0 when socketType == SocketType.Stream => ProtocolType.Tcp,
0 when socketType == SocketType.Dgram => ProtocolType.Udp,
6 => ProtocolType.Tcp,
17 => ProtocolType.Udp,
_ => ProtocolType.Unknown,
};
return addressFamily != AddressFamily.Unspecified &&
socketType != SocketType.Unknown &&
protocolType != ProtocolType.Unknown;
}
private static bool TryReadSocketAddress(CpuContext ctx, ulong address, int length, out IPEndPoint endpoint)
{
endpoint = new IPEndPoint(IPAddress.Any, 0);
if (address == 0 || length < 16)
{
return false;
}
Span<byte> bytes = stackalloc byte[16];
if (!ctx.Memory.TryRead(address, bytes) || bytes[1] != 2)
{
return false;
}
var port = BinaryPrimitives.ReadUInt16BigEndian(bytes[2..4]);
endpoint = new IPEndPoint(new IPAddress(bytes[4..8]), port);
return true;
}
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0)
{
return true;
}
Span<byte> one = stackalloc byte[1];
var bytes = new byte[maxLength];
var count = 0;
for (; count < maxLength; count++)
{
if (!ctx.Memory.TryRead(address + (ulong)count, one))
{
return false;
}
if (one[0] == 0)
{
break;
}
bytes[count] = one[0];
}
value = Encoding.UTF8.GetString(bytes, 0, count);
return true;
}
private static void TraceNet(string operation, int id, ulong arg0, ulong arg1, ulong arg2)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] net.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16}");
}
}