Skip to content

Memory-safety issues in WUD/WUHB/FSA parsers reachable from malformed title files #1980

Description

@1e1

Summary

A few memory-safety issues in the disc/title/homebrew parsers are reachable purely by loading a crafted or corrupt file. Severity is moderate (no RCE demonstrated, and the guest address space is a fixed mapping), but they are easy to trigger and cheap to harden.

1. WUHB — divide-by-zero on a zero-sized hash table

WUHBReader::GetHashTableEntryOffset (src/Cafe/Filesystem/WUHB/WUHBReader.cpp:117):

const uint64 hash_table_entry_count = hash_table_size / sizeof(uint32);
... (hash % hash_table_entry_count) ...

A header with *_hash_table_size < 4 makes hash_table_entry_count == 0% 0 (SIGFPE) on the first file lookup. hash_table_size is never validated.

2. WUHB — unbounded hash-chain traversal

WUHBReader::SearchHashList (:134) follows entryOffset = entry.hash in a for(;;) with no visited set or iteration cap; two entries referencing each other loop forever (each iteration doing file I/O).

3. WUHB — directory name copy not NUL-terminated

src/Cafe/Filesystem/fscDeviceWuhb.cpp:78 and :91:

std::strncpy(dirEntry->path, entry.name.c_str(), FSC_MAX_DIR_NAME_LENGTH);

strncpy with count == sizeof(dest) writes no terminator when the (file-controlled) name length ≥ FSC_MAX_DIR_NAME_LENGTH. HostFS and WUD both truncate-and-terminate; WUHB is the outlier.

4. FSA ReadDir — unbounded strcpy of the directory entry name

src/Cafe/IOSU/fsa/iosu_fsa.cpp:559:

strcpy(dirEntryOut->name, fscDirEntry.path);

copies from a 256-byte source (FSCDirEntry.path) into the 128-byte guest FSDirEntry_t.name; a 128–255 byte filename overflows the guest response. The getCwd handler just below already uses a bounded strncpy + explicit NUL and is the pattern to follow.

5. WUD/WUX — 32-bit multiply truncation → under-allocation → OOB

src/Cafe/Filesystem/WUD/wud.cpp:46:

unsigned int indexTableSize = sizeof(unsigned int) * wud->indexTableEntryCount;

indexTableEntryCount derives from the file-controlled uncompressedSize/sectorSize. The product is computed as size_t but truncated into a 32-bit unsigned int, so a large uncompressedSize wraps to a small value; the following malloc (also unchecked for null) under-allocates and later indexTable[sectorIndex] reads out of bounds. uncompressedSize is not range-validated.

Suggested hardening

Validate the sizes against the actual file length before allocating, compute in 64-bit + null-check the allocation, bound the hash-chain loop, and use bounded/NUL-terminated copies for the name fields. Happy to help if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions