fix(agc): accept unaligned shader headers

Driver::RegisterShader required the Shader header at an 8-byte
boundary (alignof(Shader)) and read its fields through the pointer.
PPSA26344 uses its shader packs in place: its compute shader objects
point at headers 4 and 1 bytes past an 8-byte boundary, so the first
sceAgcCreateShader of such a header threw "null or misaligned address".

The header is now checked for null and readable memory at any
address, and its fixed fields and the registers the trace prints are
copied out with memcpy. The code address must still be 256-byte
aligned, and the header and size checks are unchanged. The console's
rule is unknown and is listed in TechnicalDebt.
This commit is contained in:
Celegans12 2026-10-06 15:24:35 +02:00
parent 148b888c3a
commit 4e18def4c1
3 changed files with 56 additions and 15 deletions

View File

@ -163,25 +163,34 @@ std::uint64_t NullPixelProgramAddress() {
void Driver::RegisterShader(const Shader* shader) {
CheckFailure();
GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader));
require(shader->file_header == 0x34333231u && shader->version == 0x18u, "invalid shader header");
require(shader->header_size >= sizeof(Shader), "shader header is smaller than its fixed fields");
require(shader->shader_size != 0 && (shader->shader_size & 3u) == 0, "invalid shader size");
GuestMemory::CheckRange(shader, shader->header_size, alignof(Shader));
const auto* code = const_cast<const void*>(shader->code);
GuestMemory::CheckRange(code, shader->shader_size, 256);
ShaderSnapshot snapshot{reinterpret_cast<std::uintptr_t>(code), reinterpret_cast<std::uintptr_t>(shader), shader->type, {}, {}};
snapshot.code.resize(shader->shader_size / sizeof(std::uint32_t));
std::memcpy(snapshot.code.data(), code, shader->shader_size);
snapshot.header.resize(shader->header_size);
std::memcpy(snapshot.header.data(), shader, shader->header_size);
GuestMemory::CheckRange(shader, sizeof(Shader), 1);
Shader fields;
std::memcpy(&fields, static_cast<const void*>(shader), sizeof(Shader));
require(fields.file_header == 0x34333231u && fields.version == 0x18u, "invalid shader header");
require(fields.header_size >= sizeof(Shader), "shader header is smaller than its fixed fields");
require(fields.shader_size != 0 && (fields.shader_size & 3u) == 0, "invalid shader size");
GuestMemory::CheckRange(shader, fields.header_size, 1);
const auto* code = const_cast<const void*>(fields.code);
GuestMemory::CheckRange(code, fields.shader_size, 256);
ShaderSnapshot snapshot{reinterpret_cast<std::uintptr_t>(code), reinterpret_cast<std::uintptr_t>(shader), fields.type, {}, {}};
snapshot.code.resize(fields.shader_size / sizeof(std::uint32_t));
std::memcpy(snapshot.code.data(), code, fields.shader_size);
snapshot.header.resize(fields.header_size);
std::memcpy(snapshot.header.data(), static_cast<const void*>(shader), fields.header_size);
static const char* traceRegs = std::getenv("APS5_TRACE_SHADER_REGS");
if (traceRegs != nullptr && (std::string(traceRegs) == "all" || std::strtoull(traceRegs, nullptr, 16) == snapshot.codeAddress)) {
std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast<unsigned long long>(snapshot.codeAddress), shader->type);
for (std::uint32_t i = 0; i < shader->num_cx_registers && shader->cx_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->cx_registers[i].offset, shader->cx_registers[i].value);
const auto print = [](const ShaderRegister* registers, std::uint32_t count) {
for (std::uint32_t i = 0; i < count && registers != nullptr; ++i) {
ShaderRegister value;
std::memcpy(&value, static_cast<const void*>(registers + i), sizeof(value));
std::fprintf(stderr, " %x=%08x", value.offset, value.value);
}
};
std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast<unsigned long long>(snapshot.codeAddress), fields.type);
print(fields.cx_registers, fields.num_cx_registers);
std::fprintf(stderr, " sh");
for (std::uint32_t i = 0; i < shader->num_sh_registers && shader->sh_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->sh_registers[i].offset, shader->sh_registers[i].value);
print(fields.sh_registers, fields.num_sh_registers);
std::fprintf(stderr, "\n");
}
std::lock_guard lock(mutex);

View File

@ -11,7 +11,9 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
@ -374,6 +376,34 @@ void testMultiSubmissions() {
}
}
void testShaderHeaderAlignment() {
alignas(256) static const std::array<std::uint32_t, 64> code{0xbf810000};
alignas(8) static std::array<std::byte, 2 * sizeof(Shader)> storage{};
Shader shader{};
shader.file_header = 0x34333231;
shader.version = 0x18;
shader.header_size = sizeof(Shader);
shader.shader_size = sizeof(code);
shader.code = code.data();
const auto at = [](std::size_t offset, const Shader& fields) {
std::memcpy(storage.data() + offset, &fields, sizeof(fields));
return reinterpret_cast<const Shader*>(storage.data() + offset);
};
for (const std::size_t offset : {0, 4, 1}) AgcDriverRegisterShader_nid_postfix(at(offset, shader));
const auto refused = [](const std::string& message, const char* reason) { check(message.find(reason) != std::string::npos, message.c_str()); };
refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(nullptr); }), "null or misaligned address");
refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(reinterpret_cast<const Shader*>(0x1001)); }), "not readable");
Shader misplaced = shader;
misplaced.code = code.data() + 1;
refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, misplaced)); }), "null or misaligned address");
Shader older = shader;
older.version = 0x17;
refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(1, older)); }), "invalid shader header");
Shader truncated = shader;
truncated.header_size = sizeof(Shader) - 4;
refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, truncated)); }), "smaller than its fixed fields");
}
void testWorkerFailure() {
std::array<std::uint32_t, 5> words{0xc0031500, 1, 1, 1, 0x41};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
@ -470,6 +500,7 @@ int main() {
testWaitFreeSubmissionBehindHeldOne();
testWaitFreeSubmissionTheCpuWaitsFor();
testMultiSubmissions();
testShaderHeaderAlignment();
testWorkerFailure();
check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure");
std::puts("AGC driver submit tests passed");

View File

@ -165,6 +165,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown
- [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes)
- [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws
- [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown
- [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown
- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does
- [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty