Merge pull request #5255 from Nesyth/fix/libkernel-apr-read-past-eof-v2
fix(libkernel): zero-fill APR reads that reach the end of the file
This commit is contained in:
commit
94f96c8e2c
|
|
@ -168,7 +168,8 @@ void _readFile(const Apr::ReadFileCommand& command) {
|
|||
stream.read(reinterpret_cast<char*>(command.destination), static_cast<std::streamsize>(command.size));
|
||||
if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string());
|
||||
const auto read = static_cast<std::uint64_t>(stream.gcount());
|
||||
if (read != command.size) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes");
|
||||
if (read != command.size && (!stream.eof() || command.offset + read < std::filesystem::file_size(file.path))) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes");
|
||||
if (read != command.size) std::memset(reinterpret_cast<char*>(command.destination) + read, 0, static_cast<std::size_t>(command.size - read));
|
||||
}
|
||||
|
||||
void _writeAddress(const Apr::WriteAddressCommand& command) {
|
||||
|
|
|
|||
|
|
@ -651,6 +651,70 @@ void TestZeroFilledBuffer() {
|
|||
std::remove(path);
|
||||
}
|
||||
|
||||
void TestReadPastEndOfFileZeroFills() {
|
||||
const std::filesystem::path directory = "ampr_read_past_end";
|
||||
std::filesystem::remove_all(directory);
|
||||
Require(std::filesystem::create_directory(directory));
|
||||
const auto file = directory / "settings.ini";
|
||||
{
|
||||
std::ofstream stream(file, std::ios::binary);
|
||||
for (std::size_t offset = 0; offset < 64; ++offset) stream.put(static_cast<char>(FileByte(offset)));
|
||||
}
|
||||
const std::string filePath = file.generic_string();
|
||||
const char* path = filePath.c_str();
|
||||
std::uint32_t fileId = 0;
|
||||
std::uint64_t size = 0;
|
||||
std::uint32_t failed = 0;
|
||||
Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(&path, 1, &fileId, &size, &failed) == 0 && size == 64);
|
||||
{
|
||||
std::ofstream stream(file, std::ios::binary | std::ios::trunc);
|
||||
for (std::size_t offset = 1; offset < size; ++offset) stream.put(static_cast<char>(FileByte(offset)));
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, 64> whole;
|
||||
std::array<std::uint8_t, 24> continued;
|
||||
std::array<std::uint8_t, 16> atEnd;
|
||||
std::array<std::uint8_t, 16> pastEnd;
|
||||
whole.fill(0xA5);
|
||||
continued.fill(0xA5);
|
||||
atEnd.fill(0xA5);
|
||||
pastEnd.fill(0xA5);
|
||||
Recorder recorder;
|
||||
Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, whole.data(), size, 0) == 0);
|
||||
Require(sceAmprAprCommandBufferReadFileScatter(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, continued.data(), 16) == 0);
|
||||
Require(sceAmprAprCommandBufferReadFileGather(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, 8, 0) == 0);
|
||||
Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, atEnd.data(), atEnd.size(), size - 1) == 0);
|
||||
Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, pastEnd.data(), pastEnd.size(), 4096) == 0);
|
||||
Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0);
|
||||
Require(MatchesFile(whole.data(), 1, size - 1) && whole[size - 1] == 0);
|
||||
for (std::size_t index = 0; index < 16; ++index) Require(continued[index] == 0);
|
||||
Require(MatchesFile(continued.data() + 16, 1, 8));
|
||||
for (const auto byte : atEnd) Require(byte == 0);
|
||||
for (const auto byte : pastEnd) Require(byte == 0);
|
||||
std::filesystem::remove_all(directory);
|
||||
}
|
||||
|
||||
void TestReadAtUnseekableOffsetThrows() {
|
||||
const char* path = "ampr_unseekable_offset.bin";
|
||||
{ std::ofstream file(path, std::ios::binary); file << "data"; }
|
||||
std::uint32_t fileId = 0;
|
||||
std::uint32_t failed = 0;
|
||||
Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0);
|
||||
std::array<std::uint8_t, 4> data{};
|
||||
const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, fileId, 0, reinterpret_cast<std::uint64_t>(data.data()), data.size(), std::uint64_t{1} << 63u};
|
||||
alignas(8) std::array<std::uint8_t, sizeof(command)> memory{};
|
||||
std::memcpy(memory.data(), &command, sizeof(command));
|
||||
Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 1, sizeof(memory), memory.data()};
|
||||
bool rejected = false;
|
||||
try {
|
||||
sceKernelAprSubmitCommandBuffer(&buffer, 0);
|
||||
} catch (const std::runtime_error& error) {
|
||||
rejected = std::string(error.what()).find("returned 0 of 4 bytes") != std::string::npos;
|
||||
}
|
||||
Require(rejected);
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
void TestResolveFailures() {
|
||||
const char* paths[]{"ampr_resolve_existing.bin", "ampr_resolve_missing.bin"};
|
||||
{ std::ofstream file(paths[0], std::ios::binary); file << "data"; }
|
||||
|
|
@ -1403,6 +1467,8 @@ int main() {
|
|||
TestClearBuffer();
|
||||
TestConstructed();
|
||||
TestZeroFilledBuffer();
|
||||
TestReadPastEndOfFileZeroFills();
|
||||
TestReadAtUnseekableOffsetThrows();
|
||||
TestResolveFailures();
|
||||
TestMalformedCommandSizes();
|
||||
TestResolveSymbolicLink();
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
- [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer.
|
||||
- [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified.
|
||||
- [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix`
|
||||
- [APR reads past the end of a file](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - a read that reaches the current end of the file on disk copies the bytes the file has and fills the rest of the destination with zeros; a read that comes back short before that end still throws. PPSA18605 resolves its settings file once, rewrites it one byte shorter and reads it again with the size that resolve returned. The console's behaviour for a read past the end of a file is unverified: it may zero-fill, leave the destination untouched or fail the command
|
||||
- [AMPR counter access](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded
|
||||
- [APR gather and scatter reads](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE`
|
||||
- [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved on first use at the top of the guest arena, above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output
|
||||
|
|
|
|||
Loading…
Reference in New Issue