From a8be1daca47ed52166dae8168e8be6566b4bbd27 Mon Sep 17 00:00:00 2001 From: can Date: Thu, 16 Jul 2026 16:24:34 +0300 Subject: [PATCH] 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. --- src/SharpEmu.Libs/Network/NetExports.cs | 16 +++- .../Network/NetExportsTests.cs | 94 +++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Network/NetExportsTests.cs diff --git a/src/SharpEmu.Libs/Network/NetExports.cs b/src/SharpEmu.Libs/Network/NetExports.cs index c80a7fc7..17f75fad 100644 --- a/src/SharpEmu.Libs/Network/NetExports.cs +++ b/src/SharpEmu.Libs/Network/NetExports.cs @@ -344,8 +344,10 @@ public static class NetExports 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 ctx.SetReturn(0); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( @@ -356,8 +358,10 @@ public static class NetExports 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 ctx.SetReturn(0); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( @@ -368,8 +372,10 @@ public static class NetExports 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 ctx.SetReturn(0); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( @@ -380,8 +386,10 @@ public static class NetExports 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 ctx.SetReturn(0); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( diff --git a/tests/SharpEmu.Libs.Tests/Network/NetExportsTests.cs b/tests/SharpEmu.Libs.Tests/Network/NetExportsTests.cs new file mode 100644 index 00000000..08aa0103 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Network/NetExportsTests.cs @@ -0,0 +1,94 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Network; +using Xunit; + +namespace SharpEmu.Libs.Tests.Network; + +// The libSceNet byte-order helpers take their operand in Rdi and return the converted value in +// Rax. They swap endianness unconditionally, which is correct on the little-endian hosts (and +// little-endian guest) the emulator targets, so network (big-endian) order is always a byte swap. +public sealed class NetExportsTests +{ + private readonly CpuContext _ctx = new(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5); + + [Fact] + public void Htonl_SwapsAllFourBytes() + { + _ctx[CpuRegister.Rdi] = 0x01020304; + + Assert.Equal(0, NetExports.NetHtonl(_ctx)); + Assert.Equal(0x04030201UL, _ctx[CpuRegister.Rax]); + } + + [Fact] + public void Ntohl_SwapsAllFourBytes() + { + _ctx[CpuRegister.Rdi] = 0x01020304; + + Assert.Equal(0, NetExports.NetNtohl(_ctx)); + Assert.Equal(0x04030201UL, _ctx[CpuRegister.Rax]); + } + + [Fact] + public void Htons_SwapsLowTwoBytesOnly() + { + // High bits above the 16-bit short must be ignored, not folded into the result. + _ctx[CpuRegister.Rdi] = 0xFFFF_0102; + + Assert.Equal(0, NetExports.NetHtons(_ctx)); + Assert.Equal(0x0201UL, _ctx[CpuRegister.Rax]); + } + + [Fact] + public void Ntohs_SwapsLowTwoBytesOnly() + { + _ctx[CpuRegister.Rdi] = 0xFFFF_0102; + + Assert.Equal(0, NetExports.NetNtohs(_ctx)); + Assert.Equal(0x0201UL, _ctx[CpuRegister.Rax]); + } + + [Fact] + public void Htonl_IgnoresBitsAboveThe32BitWord() + { + _ctx[CpuRegister.Rdi] = 0xDEADBEEF_01020304; + + Assert.Equal(0, NetExports.NetHtonl(_ctx)); + Assert.Equal(0x04030201UL, _ctx[CpuRegister.Rax]); + } + + [Theory] + [InlineData(0xDEADBEEFUL)] + [InlineData(0x00000000UL)] + [InlineData(0xFFFFFFFFUL)] + [InlineData(0x00000001UL)] + public void HtonlThenNtohl_RoundTripsToOriginal(ulong value) + { + _ctx[CpuRegister.Rdi] = value; + NetExports.NetHtonl(_ctx); + + _ctx[CpuRegister.Rdi] = _ctx[CpuRegister.Rax]; + NetExports.NetNtohl(_ctx); + + Assert.Equal(value, _ctx[CpuRegister.Rax]); + } + + // Regression guard: a non-palindromic value must not come back as 0. The functions previously + // computed the swap into Rax and then called SetReturn(0), which overwrote Rax, so every + // sceNetHtonl/Htons/Ntohl/Ntohs call returned 0 regardless of input. + [Fact] + public void ByteOrderConversions_DoNotReturnZeroForNonZeroInput() + { + _ctx[CpuRegister.Rdi] = 0x01020304; + NetExports.NetHtonl(_ctx); + Assert.NotEqual(0UL, _ctx[CpuRegister.Rax]); + + _ctx[CpuRegister.Rax] = 0; + _ctx[CpuRegister.Rdi] = 0x0102; + NetExports.NetHtons(_ctx); + Assert.NotEqual(0UL, _ctx[CpuRegister.Rax]); + } +}