Pad: implement scePadGetTriggerEffectState under its own NID (#712)

NID znaWI0gpuo8 was mapped to sceUserServiceGetUserName as a "title-captured
alias". It is not that symbol. Recomputing the NID of every catalogued name
(base64 of the reversed first eight sha1 bytes of name+salt) resolves
znaWI0gpuo8 to scePadGetTriggerEffectState, and sceUserServiceGetUserName
hashes to 1xxcMiGu2fo instead. Auditing all 1087 export declarations the same
way found this to be the only NID whose declared name is wrong.

The consequence was not a missing export but a wrong one: the user-service
handler rejected the pad's arguments and returned
SCE_USER_SERVICE_ERROR_INVALID_PARAMETER about eighteen thousand times per
run in PPSA10112, so every poll fell back to a cached button bitmask. It
also hid the calls from every search for pad activity, which is why this
title was believed never to touch scePad at all.

The state size is taken from the caller's own frame rather than assumed: the
guest points the out-param at rbp-0x30 and stores its stack cookie at
rbp-0x28, leaving eight bytes for the state. Writing the sixteen the frame
superficially suggests would land on the cookie and fail the guest's stack
check - the same failure this codebase has already hit three times from
oversized HLE writes - so the test pins the size by asserting the cookie
survives.

No host pad exposes DualSense adaptive-trigger feedback, so the neutral
all-zero state is reported as success, which lets the caller take its normal
path instead of the fallback.
This commit is contained in:
kuba
2026-07-31 11:09:24 +02:00
committed by GitHub
parent b75e4e01a0
commit b572738547
3 changed files with 79 additions and 10 deletions
+36
View File
@@ -373,6 +373,42 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
} }
// Size taken from the caller's own frame rather than assumed: the guest
// reserves 0x10 bytes, points the out-param at rbp-0x30, and stores its
// stack cookie at rbp-0x28, so only eight bytes belong to the state. A
// sixteen-byte write would land on the cookie and fail the stack check.
private const int TriggerEffectStateSize = 8;
[SysAbiExport(
Nid = "znaWI0gpuo8",
ExportName = "scePadGetTriggerEffectState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadGetTriggerEffectState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var stateAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (stateAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// No host pad exposes DualSense adaptive-trigger feedback, so every
// trigger reports the neutral "no effect engaged" state. Reporting it
// as success is what lets the caller take its normal path instead of
// falling back to a cached button bitmask every poll.
Span<byte> state = stackalloc byte[TriggerEffectStateSize];
state.Clear();
return ctx.Memory.TryWrite(stateAddress, state)
? ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static HostAdaptiveTriggerEffect DecodeTriggerEffect(ReadOnlySpan<byte> command) private static HostAdaptiveTriggerEffect DecodeTriggerEffect(ReadOnlySpan<byte> command)
{ {
var mode = BinaryPrimitives.ReadUInt32LittleEndian(command); var mode = BinaryPrimitives.ReadUInt32LittleEndian(command);
@@ -144,16 +144,6 @@ public static class UserServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
// Title-captured alias NID for the same username query.
#pragma warning disable SHEM004
[SysAbiExport(
Nid = "znaWI0gpuo8",
ExportName = "sceUserServiceGetUserName",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetUserNameAlt(CpuContext ctx) => UserServiceGetUserName(ctx);
#pragma warning restore SHEM004
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued. // Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
#pragma warning disable SHEM006 #pragma warning disable SHEM006
[SysAbiExport( [SysAbiExport(
@@ -30,4 +30,47 @@ public sealed class PadExportsTests
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle); _ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx)); Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx));
} }
/// <summary>
/// Mirrors the calling frame observed in PPSA10112: the out-param points at
/// rbp-0x30 and the caller's stack cookie sits at rbp-0x28, so the state is
/// eight bytes. Writing more would smash the cookie and fail the guest's
/// stack check, which is the failure mode this size guards against.
/// </summary>
[Fact]
public void GetTriggerEffectState_WritesEightBytesAndLeavesTheCookieIntact()
{
const ulong stateAddress = Base + 0x100;
const ulong cookieAddress = stateAddress + 8;
const ulong cookie = 0xC0DEC0DECAFEBA00UL;
Assert.True(_memory.TryWrite(stateAddress, new byte[] { 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE }));
Assert.True(_memory.TryWrite(cookieAddress, BitConverter.GetBytes(cookie)));
_ctx[CpuRegister.Rdi] = 0;
_ctx[CpuRegister.Rsi] = stateAddress;
Assert.Equal(0, PadExports.PadGetTriggerEffectState(_ctx));
Span<byte> state = stackalloc byte[8];
Assert.True(_memory.TryRead(stateAddress, state));
foreach (var value in state)
{
Assert.Equal(0, value);
}
Span<byte> guard = stackalloc byte[8];
Assert.True(_memory.TryRead(cookieAddress, guard));
Assert.Equal(cookie, BitConverter.ToUInt64(guard));
}
[Theory]
[InlineData(2)]
[InlineData(-1)]
public void GetTriggerEffectState_RejectsForeignHandles(int handle)
{
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
_ctx[CpuRegister.Rsi] = Base + 0x100;
Assert.Equal(InvalidHandle, PadExports.PadGetTriggerEffectState(_ctx));
}
} }