mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-06-22 12:32:28 -05:00
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:
parent
9e7d340f22
commit
d35fe1b78b
|
|
@ -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"})
|
||||
{
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user