IOS/FS: Rewrite NAND savestating

This makes us savestate the NAND using the same APIs the guest uses
instead of directly touching the host files. This solves several
problems:

* If the user loaded a malicious savestate, it could use path traversal
  to overwrite arbitrary files on the host file system. (Reported by
  MrSynAckster.)
* Metadata (UID, GID, attribute, modes) wasn't being savestated.
* NAND redirects weren't handled, except for NAND redirects at the root
  of where the savestate was being done. (This only possibly matters if
  TASing a Riivolution patch. The root of the savestate is at /tmp when
  not TASing, and the only case where we do a NAND redirect is inside
  /title if requested by a Riivolution patch.)
This commit is contained in:
JosJuice 2026-05-30 15:12:17 +02:00
parent 9e7d340f22
commit d35fe1b78b
5 changed files with 212 additions and 94 deletions

View File

@ -16,6 +16,7 @@
#endif
#include "Common/CommonTypes.h"
#include "Common/EnumFormatter.h"
class PointerWrap;
@ -281,6 +282,10 @@ public:
virtual Result<ExtendedDirectoryStats> GetExtendedDirectoryStats(const std::string& path) = 0;
virtual void SetNandRedirects(std::vector<NandRedirect> nand_redirects) = 0;
protected:
void DoStateWriteOrMeasure(PointerWrap& p, const std::string& directory_path);
void DoStateRead(PointerWrap& p, const std::string& directory_path);
};
template <typename T>
@ -319,3 +324,16 @@ IOS::HLE::ReturnCode ConvertResult(ResultCode code);
} // namespace FS
} // namespace IOS::HLE
template <>
struct fmt::formatter<IOS::HLE::FS::ResultCode> : EnumFormatter<IOS::HLE::FS::ResultCode::ShortRead>
{
constexpr formatter()
: EnumFormatter({"Success", "Invalid", "AccessDenied", "SuperblockWriteFailed",
"SuperblockInitFailed", "AlreadyExists", "NotFound", "FstFull",
"NoFreeSpace", "NoFreeHandle", "TooManyPathComponents", "InUse", "BadBlock",
"EccError", "CriticalEccError", "FileNotEmpty", "CheckFailed",
"UnknownError", "ShortRead"})
{
}
};

View File

@ -4,6 +4,7 @@
#include "Core/IOS/FS/FileSystem.h"
#include <algorithm>
#include <array>
#include <expected>
#include "Common/Assert.h"
@ -13,6 +14,8 @@
namespace IOS::HLE::FS
{
constexpr u32 BUFFER_CHUNK_SIZE = 65536;
bool IsValidPath(std::string_view path)
{
return path == "/" || IsValidNonRootPath(path);
@ -133,4 +136,194 @@ ResultCode FileSystem::CreateFullPath(Uid uid, Gid gid, const std::string& path,
++position;
}
}
void FileSystem::DoStateRead(PointerWrap& p, const std::string& directory_path)
{
const ResultCode delete_result = Delete(0, 0, directory_path);
if (delete_result != ResultCode::Success && delete_result != ResultCode::NotFound)
{
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call Delete: {}", delete_result);
p.SetVerifyMode();
return;
}
Metadata metadata;
p.Do(metadata.uid);
p.Do(metadata.gid);
p.Do(metadata.attribute);
p.Do(metadata.modes);
p.Do(metadata.is_file);
p.Do(metadata.size);
p.Do(metadata.fst_index);
const ResultCode create_directory_result = CreateDirectory(
metadata.uid, metadata.gid, directory_path, metadata.attribute, metadata.modes);
if (create_directory_result != ResultCode::Success)
{
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call CreateDirectory: {}",
create_directory_result);
p.SetVerifyMode();
return;
}
// Now restore from the stream
std::vector<std::string> children;
p.DoEachElement(children, [this, &directory_path](PointerWrap& p_, std::string& child_name) {
Metadata child_metadata;
p_.Do(child_metadata);
p_.Do(child_name);
std::string child_path;
child_path.reserve(directory_path.size() + child_name.size() + 1);
child_path.append(directory_path);
if (directory_path.back() != '/')
child_path.push_back('/');
child_path.append(child_name);
if (child_metadata.is_file)
{
const ResultCode create_file_result =
CreateFile(child_metadata.uid, child_metadata.gid, child_path, child_metadata.attribute,
child_metadata.modes);
if (create_file_result != ResultCode::Success)
{
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call CreateFile for {}: {}", child_name,
create_file_result);
p_.SetVerifyMode();
return;
}
std::array<u8, BUFFER_CHUNK_SIZE> buffer;
Result<FileHandle> handle = OpenFile(0, 0, child_path, Mode::Write);
if (!handle)
{
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call OpenFile for {}: {}", child_name,
handle.error());
p_.SetVerifyMode();
return;
}
u32 i = 0;
while (i < child_metadata.size)
{
const u32 bytes_to_write =
std::min(child_metadata.size - i, static_cast<u32>(buffer.size()));
p_.DoArray(buffer.data(), bytes_to_write);
Result<size_t> write_result = handle->Write(buffer.data(), bytes_to_write);
if (!write_result)
{
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call Write for {}: {}", child_name,
write_result.error());
p_.SetVerifyMode();
return;
}
if (*write_result != bytes_to_write)
{
ERROR_LOG_FMT(IOS_FS, "DoStateRead tried to write {} bytes to {} but wrote {} bytes",
child_name, bytes_to_write, *write_result);
p_.SetVerifyMode();
return;
}
i += bytes_to_write;
}
}
else
{
DoStateRead(p_, child_path);
}
});
}
void FileSystem::DoStateWriteOrMeasure(PointerWrap& p, const std::string& directory_path)
{
const Result<Metadata> metadata = GetMetadata(0, 0, directory_path);
if (!metadata)
{
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call GetMetadata: {}", metadata.error());
p.SetVerifyMode();
return;
}
p.Do(metadata->uid);
p.Do(metadata->gid);
p.Do(metadata->attribute);
p.Do(metadata->modes);
p.Do(metadata->is_file);
p.Do(metadata->size);
p.Do(metadata->fst_index);
auto children = ReadDirectory(0, 0, directory_path);
if (!children)
{
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call ReadDirectory: {}",
children.error());
p.SetVerifyMode();
return;
}
p.DoEachElement(*children, [this, &directory_path](PointerWrap& p_, std::string& child_name) {
std::string child_path;
child_path.reserve(directory_path.size() + child_name.size() + 1);
child_path.append(directory_path);
if (directory_path.back() != '/')
child_path.push_back('/');
child_path.append(child_name);
Result<Metadata> child_metadata = GetMetadata(0, 0, child_path);
if (!child_metadata)
{
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call GetMetadata for {}: {}",
child_name, child_metadata.error());
p_.SetVerifyMode();
return;
}
p_.Do(*child_metadata);
p_.Do(child_name);
if (child_metadata->is_file)
{
std::array<u8, BUFFER_CHUNK_SIZE> buffer;
Result<FileHandle> handle = OpenFile(0, 0, child_path, Mode::Read);
if (!handle)
{
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call OpenFile for {}: {}",
child_name, handle.error());
p_.SetVerifyMode();
return;
}
u32 i = 0;
while (i < child_metadata->size)
{
const u32 bytes_to_read =
std::min(child_metadata->size - i, static_cast<u32>(buffer.size()));
Result<size_t> read_result = handle->Read(buffer.data(), bytes_to_read);
if (!read_result)
{
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call Read for {}: {}", child_name,
read_result.error());
p_.SetVerifyMode();
return;
}
if (*read_result != bytes_to_read)
{
ERROR_LOG_FMT(IOS_FS,
"DoStateWriteOrMeasure tried to read {} bytes from {} but got {} bytes",
child_name, bytes_to_read, *read_result);
p_.SetVerifyMode();
return;
}
p_.DoArray(buffer.data(), bytes_to_read);
i += bytes_to_read;
}
}
else
{
DoStateWriteOrMeasure(p_, child_path);
}
});
}
} // namespace IOS::HLE::FS

View File

@ -29,8 +29,6 @@
namespace IOS::HLE::FS
{
constexpr u32 BUFFER_CHUNK_SIZE = 65536;
HostFileSystem::HostFilename HostFileSystem::BuildFilename(const std::string& wii_path) const
{
for (const auto& redirect : m_nand_redirects)
@ -279,94 +277,6 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string&
return entry;
}
void HostFileSystem::DoStateRead(PointerWrap& p, const std::string& start_directory_path)
{
std::string path = BuildFilename(start_directory_path).host_path;
File::DeleteDirRecursively(path);
File::CreateDir(path);
// now restore from the stream
while (true)
{
char type = 0;
p.Do(type);
if (!type)
break;
std::string file_name;
p.Do(file_name);
std::string name = path + "/" + file_name;
switch (type)
{
case 'd':
{
File::CreateDir(name);
break;
}
case 'f':
{
u32 size = 0;
p.Do(size);
File::IOFile handle(name, "wb");
char buf[BUFFER_CHUNK_SIZE];
u32 count = size;
while (count > BUFFER_CHUNK_SIZE)
{
p.DoArray(buf);
handle.WriteArray(&buf[0], BUFFER_CHUNK_SIZE);
count -= BUFFER_CHUNK_SIZE;
}
p.DoArray(&buf[0], count);
handle.WriteArray(&buf[0], count);
break;
}
}
}
}
void HostFileSystem::DoStateWriteOrMeasure(PointerWrap& p, const std::string& start_directory_path)
{
std::string path = BuildFilename(start_directory_path).host_path;
File::FSTEntry parent_entry = File::ScanDirectoryTree(path, true);
std::deque<File::FSTEntry> todo;
todo.insert(todo.end(), parent_entry.children.begin(), parent_entry.children.end());
while (!todo.empty())
{
File::FSTEntry& entry = todo.front();
std::string name = entry.physicalName;
name.erase(0, path.length() + 1);
char type = entry.isDirectory ? 'd' : 'f';
p.Do(type);
p.Do(name);
if (entry.isDirectory)
{
todo.insert(todo.end(), entry.children.begin(), entry.children.end());
}
else
{
u32 size = (u32)entry.size;
p.Do(size);
File::IOFile handle(entry.physicalName, "rb");
char buf[BUFFER_CHUNK_SIZE];
u32 count = size;
while (count > BUFFER_CHUNK_SIZE)
{
handle.ReadArray(&buf[0], BUFFER_CHUNK_SIZE);
p.DoArray(buf);
count -= BUFFER_CHUNK_SIZE;
}
handle.ReadArray(&buf[0], count);
p.DoArray(&buf[0], count);
}
todo.pop_front();
}
char type = 0;
p.Do(type);
}
void HostFileSystem::DoState(PointerWrap& p)
{
// Temporarily close the file, to prevent any issues with the savestating of files/folders.

View File

@ -60,9 +60,6 @@ public:
void SetNandRedirects(std::vector<NandRedirect> nand_redirects) override;
private:
void DoStateWriteOrMeasure(PointerWrap& p, const std::string& start_directory_path);
void DoStateRead(PointerWrap& p, const std::string& start_directory_path);
struct FstEntry
{
bool CheckPermission(Uid uid, Gid gid, Mode requested_mode) const;

View File

@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 190; // Last changed in PR 14448
constexpr u32 STATE_VERSION = 191; // Last changed in PR 14668
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217