Files
ppsspp/Common/Serialize/SerializeFuncs.h
T
Henrik RydgårdandClaude Opus 5 b42a03e095 Add savestate serializer tests, fix three bounds-check bugs
PointerWrap and the Do() overloads around it are how every savestate is
written and read, and had no direct coverage. Everything read back came off
disk, so the corrupt-input paths matter as much as the round trips.

Three bugs, all in the bounds checking added in 58d4759ceb:

1. sizeof(T) is not a lower bound on how many bytes an element serializes to.
   It only holds for the types DoHelper_ writes out raw. A std::string is 32-40
   bytes in memory and serializes to as few as five; a T* serializes to whatever
   T::DoState() writes. So DoVector/DoList/DoSet/DoMap could reject a perfectly
   valid savestate whenever count * sizeof(element) exceeded the bytes left in
   the buffer. That is not hypothetical: pspFileSystem is serialized dead last
   in SaveStart::DoState, and MetaFileSystem::DoState does Do(p, currentDir) on
   a std::map<int, std::string>, so the check runs with only a few hundred bytes
   remaining and claims 44 bytes per entry against roughly 22 actual. Added
   SerializeMinElemSize<T>(), mirroring DoHelper_'s own condition, and used it
   in all five containers. The bound is only loosened, so nothing that loaded
   before can stop loading.

2. Do(p, std::map<K, T *> &) deletes every value before reading the new ones,
   and DoMap then returned on a bad count without clearing - leaving the map
   full of freed pointers to be used or deleted again. Six live maps go through
   this (sceMpeg, sceMp3, sceAac, sceFont, sceHeap, sceKernelThread's pending
   calls), so a corrupt savestate meant a use-after-free. Clear before the guard
   can bail out, in DoMap, DoMultimap and DoSet.

3. The wstring and u16string overloads validated stringLen < 0 but not 0, and
   didn't require a whole number of characters. read() computes
   stringLen / sizeof(char) - 1, so a length of 0 resized to SIZE_MAX and
   memcpy'd with a wrapped-around size. PSPOskDialog::DoState serializes both
   (inputChars at v2, a legacy wstring below that), so this was reachable: the
   test aborts the process without the fix.

The test covers round trips of PODs, strings (empty, embedded NUL), vector,
map, set, list and map-of-pointers, section titles and version gating in both
directions, marker mismatches, measure-vs-write checkpoint disagreement, the
error latch dropping to MODE_NOOP, every truncation of a valid buffer, and
hand-corrupted counts and lengths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:50:57 +02:00

154 lines
4.4 KiB
C++

// Copyright (C) 2003 Dolphin Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official SVN repository and contact information can be found at
// http://code.google.com/p/dolphin-emu/
#pragma once
// Templates for save state serialization. See Serializer.h.
#include <string>
#include <type_traits>
#include "Common/Serialize/Serializer.h"
#include "Common/Swap.h"
void Do(PointerWrap &p, std::string &x);
void Do(PointerWrap &p, std::wstring &x); // DEPRECATED, do not save wstrings
void Do(PointerWrap &p, std::u16string &x);
void Do(PointerWrap &p, tm &t);
// Don't use DoHelper_ directly. Just use Do().
// This makes it a compile error if you forget to define DoState() on non-POD.
// Which also can be a problem, for example struct tm is non-POD on linux, for whatever reason...
template<typename T, bool isPOD = std::is_standard_layout<T>::value && std::is_trivial<T>::value, bool isPointer = std::is_pointer<T>::value>
struct DoHelper_ {
static void DoArray(PointerWrap &p, T *x, int count) {
for (int i = 0; i < count; ++i)
Do(p, x[i]);
}
static void DoThing(PointerWrap &p, T &x) {
DoClass(p, x);
}
};
template<typename T>
struct DoHelper_<T, true, false> {
static void DoArray(PointerWrap &p, T *x, int count) {
p.DoVoid((void *)x, sizeof(T) * count);
}
static void DoThing(PointerWrap &p, T &x) {
p.DoVoid((void *)&x, sizeof(x));
}
};
template<class T>
void DoClass(PointerWrap &p, T &x) {
x.DoState(p);
}
template<class T>
void DoClass(PointerWrap &p, T *&x) {
if (p.mode == PointerWrap::MODE_READ) {
delete x;
x = new T();
}
x->DoState(p);
}
template<class T, class S, typename... Args>
void DoSubClass(PointerWrap &p, T *&x, Args... args) {
if (p.mode == PointerWrap::MODE_READ) {
if (x != nullptr)
delete x;
x = new S(args...);
}
x->DoState(p);
}
template<class T>
void DoArray(PointerWrap &p, T *x, int count) {
DoHelper_<T>::DoArray(p, x, count);
}
// Lower bound on the bytes one element of a serialized container takes up, used to sanity check
// element counts read from a savestate against how much buffer is actually left.
// sizeof(T) is only valid for the types DoHelper_ writes out raw - the same condition as its
// specialization above. Anything with its own Do()/DoState() (a string, a pointer to a class, a
// nested container) routinely serializes far fewer bytes than it occupies in memory, and using
// sizeof(T) for those rejects perfectly good savestates.
template<class T>
constexpr size_t SerializeMinElemSize() {
return std::is_standard_layout<T>::value && std::is_trivial<T>::value && !std::is_pointer<T>::value ? sizeof(T) : 1;
}
template<class T>
void Do(PointerWrap &p, T &x) {
DoHelper_<T>::DoThing(p, x);
}
template<class T>
void DoVector(PointerWrap &p, std::vector<T> &x, T &default_val) {
u32 vec_size = (u32)x.size();
Do(p, vec_size);
// Guard against an attacker-controlled size that would both resize the
// vector hugely and read past the end of the buffer.
if (p.mode == PointerWrap::MODE_READ || p.mode == PointerWrap::MODE_VERIFY) {
if (vec_size > p.Remaining() / SerializeMinElemSize<T>()) {
p.SetError(PointerWrap::ERROR_FAILURE);
return;
}
}
if (vec_size != x.size())
x.resize(vec_size, default_val);
if (vec_size > 0)
DoArray(p, &x[0], vec_size);
}
template<class T>
void Do(PointerWrap &p, std::vector<T *> &x) {
T *dv = nullptr;
DoVector(p, x, dv);
}
template<class T>
void Do(PointerWrap &p, std::vector<T> &x) {
T dv = T();
DoVector(p, x, dv);
}
template<class T>
void Do(PointerWrap &p, std::vector<T> &x, T &default_val) {
DoVector(p, x, default_val);
}
template<typename T, typename F>
void Do(PointerWrap &p, swap_struct_t<T, F> &x) {
T v = x.swap();
Do(p, v);
x = v;
}
template<class T>
void DoPointer(PointerWrap &p, T *&x, T *const base) {
// pointers can be more than 2^31 apart, but you're using this function wrong if you need that much range
s32 offset = x - base;
Do(p, offset);
if (p.mode == PointerWrap::MODE_READ)
x = base + offset;
}