Repository navigation
loader: reject corrupt NSO modules that fail to decompress - #863
Fernandess07 wants to merge 1 commit into
Conversation
|
Retargeted to |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
997d1c9 to
09e8ca1
Compare
| std::span<const u8>{compressed_data}.first(nso_header.segments_compressed_size[i]) | ||
| ); | ||
| ASSERT(r > 0); | ||
| if (r <= 0) { |
There was a problem hiding this comment.
This only catches outright zstd errors. ZSTD_decompress can succeed and write fewer bytes than the segment size, and the rest of decompressed_size (left over from the previous segment) then gets copied in. Could this match the LZ4 branch?
if (r != int(nso_header.segments[i].size)) {Also, DecompressDataZBIC returns -1 when dst is empty, so a compressed 0-byte segment (e.g. an empty .data) would now fail the load, where before the assert only logged. Probably worth skipping decompression when segments[i].size == 0.
There was a problem hiding this comment.
Changed the check to match the LZ4 branch and skipped size-0 segments
| return std::nullopt; | ||
| }; | ||
| for (std::size_t i = 0; i < nso_header.segments.size(); ++i) { | ||
| nso_file.Read(compressed_data.data(), nso_header.segments_compressed_size[i], nso_header.segments[i].offset); |
There was a problem hiding this comment.
The return value of Read is ignored, so a truncated dump (probably the most common kind of corruption) shows up as a decompression failure or as stale bytes from the previous segment. Since this PR is about corrupt dumps, could this be checked as well?
if (nso_file.Read(compressed_data.data(), nso_header.segments_compressed_size[i],
nso_header.segments[i].offset) != nso_header.segments_compressed_size[i]) {
return corrupt(i, "is truncated");
}There was a problem hiding this comment.
Did what you suggested, tested with an rtld cut by 200 bytes and the log shows .data is truncated.
| if (nso_header.IsSegmentHashChecked(i)) { | ||
| NSOHeader::SHA256Hash hash{}; | ||
| unsigned int hash_size = 0; | ||
| EVP_Digest(segment_data, nso_header.segments[i].size, hash.data(), &hash_size, |
There was a problem hiding this comment.
For an uncompressed segment segment_data points into compressed_data, which is sized to the largest compressed segment, but this hashes segments[i].size bytes. A corrupt header with size > segments_compressed_size[i] reads past the heap buffer here. The uncompressed memcpy below already has this bug, but this adds a second over-read. Could uncompressed segments where segments_compressed_size[i] != segments[i].size be rejected via corrupt()?
Minor: EVP_Digest's return value is ignored. That's harmless, since a failure only gives a false warning, but it's easy to check.
There was a problem hiding this comment.
Rejected uncompressed segments with mismatched sizes and now check EVP_Digest
| "Integrity verification failed.", | ||
| "The NUP container is malformed or its TOC hash does not match.", | ||
| "The NUP container does not contain a loadable base package.", | ||
| "An ExeFS module is corrupt and could not be decompressed. The game needs to be redumped.", |
There was a problem hiding this comment.
Nit: this is also shown when a standalone .nso is loaded through AppLoader_NSO::Load, which isn't ExeFS. Something like "An executable module (NSO) is corrupt…" would cover both.
There was a problem hiding this comment.
Changed the message to the one you suggested, and "decompressed" to "loaded" since it now also covers truncated segments.
| std::vector<Core::NCE::Patcher>* patches = nullptr, | ||
| s32 patch_index = -1); | ||
| s32 patch_index = -1, | ||
| ResultStatus* out_status = nullptr); |
There was a problem hiding this comment.
Nit, not a blocker: this is a tenth parameter after several defaulted ones. Returning the status (e.g. std::expected<VAddr, ResultStatus>) would avoid the out-pointer and the ResultStatus module_status = ErrorLoadingNSO; setup at each call site.
There was a problem hiding this comment.
Didn't do it in this PR to keep the diff small, but I can do it in a separate one.
…ck (Ocarina of Time foliage shadows) (Eglantine-Community#863) * vulkan, shader: draw points and lines through the mesh compute fallback The compute fallback (Eglantine-Community#569) only took mesh programs that output triangles, so programs with point or line topology were dropped on devices without VK_EXT_mesh_shader. Ocarina of Time draws its foliage shadow map (1600x1600 D32) with a point mesh program, so on those devices the map stayed empty and grass and bushes cast no shadow. The fallback now keeps PrimitiveVertices() indices per primitive (map bytes 4 + k * p, as the native epilogue reads them), draws a point, line or triangle list, and gives points the fixed state point size unless the program writes its own. A repeated index is not degenerate for points and lines, so slots past the primitive count get MESH_EMULATION_UNUSED_VERTEX and the vertex shader culls them with a negative clip distance. Moving them outside the view volume was not enough on RADV (RX 5700 XT): some of them still reached the shadow map, and the far field beyond it came out fully shadowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SDzM9hQaYmCiLmPrj79xs8 * vulkan, shader: document the functions the point and line fallback touches Doc comments for ComputeMeshEpilogue, EmitMeshEmulationVertex, GraphicsPipeline::ConfigureDraw, GraphicsPipeline::MakePipeline, RasterizerVulkan::ReadMeshEmulationProbe and PrepareDraw. No code change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SDzM9hQaYmCiLmPrj79xs8 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Previously, when an NSO module failed to decompress, the emulator got stuck in an endless loading loop, leaving the user unable to tell whether the problem was the emulator or the dump. The two ASSERTs were replaced by a check that returns a new error, ErrorCorruptNSO, which is shown to the user and written to the log. The NSO header stores the SHA-256 of each segment, but the emulator never compared it. It now does, which helps identify the problem. A mismatch only logs a warning, so games are not prevented from running when the hash does not match. Assisted-by: Claude Opus 5.5 (diagnosis of the corrupt dump, implementation and testing) Signed-off-by: Pedro Fernandes <pedrocufe05@gmail.com>
09e8ca1 to
555dbc2
Compare
Summary
Previously, when an NSO module failed to decompress, the emulator got
stuck in an endless loading loop, leaving the user unable to tell
whether the problem was the emulator or the dump.
The two ASSERTs were replaced by a check that returns a new error,
ErrorCorruptNSO, which is shown to the user and written to the log.
The NSO header stores the SHA-256 of each segment, but the emulator
never compared it. It now does, which helps identify the problem. A
mismatch only logs a warning, so games are not prevented from running
when the hash does not match.
Assisted-by: Claude Opus 5.5 (diagnosis of the corrupt dump, implementation and testing)
Type of change
Related issues
Related to #70
Testing
Games tested:
Corrupt Pokopia dump (0400A3A01EE4C000): previously stuck in an endless loading loop, now fails with an error (checked in both the Qt frontend and eden-cli).
Checklist
Screenshots / logs
Summary by CodeRabbit