2014-03-30 22:15:17 +02:00
|
|
|
/* FCE Ultra - NES/Famicom Emulator
|
|
|
|
|
*
|
|
|
|
|
* Copyright notice for this file:
|
|
|
|
|
* Copyright (C) 2002 Xodnizel
|
|
|
|
|
*
|
|
|
|
|
* 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; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* 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 for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program; if not, write to the Free Software
|
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
2021-10-20 16:31:45 +01:00
|
|
|
#include <string/stdstring.h>
|
2021-10-21 15:52:40 +01:00
|
|
|
#include <file/file_path.h>
|
2021-10-20 16:31:45 +01:00
|
|
|
#include <streams/file_stream.h>
|
|
|
|
|
|
2014-03-30 22:15:17 +02:00
|
|
|
#include "fceu-types.h"
|
|
|
|
|
#include "fceu.h"
|
|
|
|
|
#include "ppu.h"
|
|
|
|
|
|
|
|
|
|
#include "cart.h"
|
2014-03-30 22:29:30 +02:00
|
|
|
#include "fceu-memory.h"
|
2014-03-30 22:15:17 +02:00
|
|
|
#include "x6502.h"
|
|
|
|
|
|
|
|
|
|
#include "general.h"
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
This file contains all code for coordinating the mapping in of the
|
|
|
|
|
address space external to the NES.
|
|
|
|
|
It's also (ab)used by the NSF code.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
uint8 *Page[32], *VPage[8];
|
|
|
|
|
uint8 **VPageR = VPage;
|
|
|
|
|
uint8 *VPageG[8];
|
|
|
|
|
uint8 *MMC5SPRVPage[8];
|
|
|
|
|
uint8 *MMC5BGVPage[8];
|
|
|
|
|
|
|
|
|
|
static uint8 PRGIsRAM[32]; /* This page is/is not PRG RAM. */
|
|
|
|
|
|
|
|
|
|
/* 16 are (sort of) reserved for UNIF/iNES and 16 to map other stuff. */
|
|
|
|
|
static int CHRram[32];
|
|
|
|
|
static int PRGram[32];
|
|
|
|
|
|
|
|
|
|
uint8 *PRGptr[32];
|
|
|
|
|
uint8 *CHRptr[32];
|
|
|
|
|
|
|
|
|
|
uint32 PRGsize[32];
|
|
|
|
|
uint32 CHRsize[32];
|
|
|
|
|
|
|
|
|
|
uint32 PRGmask2[32];
|
|
|
|
|
uint32 PRGmask4[32];
|
|
|
|
|
uint32 PRGmask8[32];
|
|
|
|
|
uint32 PRGmask16[32];
|
|
|
|
|
uint32 PRGmask32[32];
|
|
|
|
|
|
|
|
|
|
uint32 CHRmask1[32];
|
|
|
|
|
uint32 CHRmask2[32];
|
|
|
|
|
uint32 CHRmask4[32];
|
|
|
|
|
uint32 CHRmask8[32];
|
|
|
|
|
|
|
|
|
|
int geniestage = 0;
|
|
|
|
|
|
|
|
|
|
int modcon;
|
|
|
|
|
|
|
|
|
|
uint8 genieval[3];
|
|
|
|
|
uint8 geniech[3];
|
|
|
|
|
|
|
|
|
|
uint32 genieaddr[3];
|
|
|
|
|
|
|
|
|
|
static INLINE void setpageptr(int s, uint32 A, uint8 *p, int ram) {
|
|
|
|
|
uint32 AB = A >> 11;
|
|
|
|
|
int x;
|
|
|
|
|
|
|
|
|
|
if (p)
|
|
|
|
|
for (x = (s >> 1) - 1; x >= 0; x--) {
|
|
|
|
|
PRGIsRAM[AB + x] = ram;
|
|
|
|
|
Page[AB + x] = p - A;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
for (x = (s >> 1) - 1; x >= 0; x--) {
|
|
|
|
|
PRGIsRAM[AB + x] = 0;
|
|
|
|
|
Page[AB + x] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint8 nothing[8192];
|
|
|
|
|
void ResetCartMapping(void) {
|
|
|
|
|
int x;
|
|
|
|
|
|
|
|
|
|
for (x = 0; x < 32; x++) {
|
|
|
|
|
Page[x] = nothing - x * 2048;
|
|
|
|
|
PRGptr[x] = CHRptr[x] = 0;
|
|
|
|
|
PRGsize[x] = CHRsize[x] = 0;
|
|
|
|
|
}
|
|
|
|
|
for (x = 0; x < 8; x++) {
|
|
|
|
|
MMC5SPRVPage[x] = MMC5BGVPage[x] = VPageR[x] = nothing - 0x400 * x;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetupCartPRGMapping(int chip, uint8 *p, uint32 size, int ram) {
|
|
|
|
|
PRGptr[chip] = p;
|
|
|
|
|
PRGsize[chip] = size;
|
|
|
|
|
|
|
|
|
|
PRGmask2[chip] = (size >> 11) - 1;
|
|
|
|
|
PRGmask4[chip] = (size >> 12) - 1;
|
|
|
|
|
PRGmask8[chip] = (size >> 13) - 1;
|
|
|
|
|
PRGmask16[chip] = (size >> 14) - 1;
|
|
|
|
|
PRGmask32[chip] = (size >> 15) - 1;
|
|
|
|
|
|
|
|
|
|
PRGram[chip] = ram ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetupCartCHRMapping(int chip, uint8 *p, uint32 size, int ram) {
|
|
|
|
|
CHRptr[chip] = p;
|
|
|
|
|
CHRsize[chip] = size;
|
|
|
|
|
|
|
|
|
|
CHRmask1[chip] = (size >> 10) - 1;
|
|
|
|
|
CHRmask2[chip] = (size >> 11) - 1;
|
|
|
|
|
CHRmask4[chip] = (size >> 12) - 1;
|
|
|
|
|
CHRmask8[chip] = (size >> 13) - 1;
|
|
|
|
|
|
|
|
|
|
CHRram[chip] = ram;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DECLFR(CartBR) {
|
|
|
|
|
return Page[A >> 11][A];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DECLFW(CartBW) {
|
|
|
|
|
if (PRGIsRAM[A >> 11] && Page[A >> 11])
|
|
|
|
|
Page[A >> 11][A] = V;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DECLFR(CartBROB) {
|
|
|
|
|
if (!Page[A >> 11])
|
|
|
|
|
return(X.DB);
|
|
|
|
|
else
|
|
|
|
|
return Page[A >> 11][A];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setprg2r(int r, uint32 A, uint32 V) {
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
/* If the registered chip size is < 2KB, PRGmask2[r] underflowed to
|
|
|
|
|
* 0xFFFFFFFF in SetupCartPRGMapping. Clear the page rather than
|
|
|
|
|
* indexing past PRGptr[r]. (The only currently-shipped caller that
|
|
|
|
|
* registers a chip this small is malee.c with size==2048, which
|
|
|
|
|
* gives mask==0 and works fine; this guard is defensive for any
|
|
|
|
|
* future board.) */
|
|
|
|
|
if (!PRGptr[r] || PRGsize[r] < 2048) {
|
|
|
|
|
setpageptr(2, A, NULL, PRGram[r]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2014-03-30 22:15:17 +02:00
|
|
|
V &= PRGmask2[r];
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
setpageptr(2, A, &PRGptr[r][V << 11], PRGram[r]);
|
2014-03-30 22:15:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setprg2(uint32 A, uint32 V) {
|
|
|
|
|
setprg2r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setprg4r(int r, uint32 A, uint32 V) {
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
if (!PRGptr[r] || PRGsize[r] < 4096) {
|
|
|
|
|
/* Fall back to two 2KB pages if size is at least 2KB; else
|
|
|
|
|
* clear both 2KB pages. */
|
|
|
|
|
if (PRGptr[r] && PRGsize[r] >= 2048) {
|
|
|
|
|
uint32 mask2 = PRGmask2[r];
|
|
|
|
|
uint32 VA = V << 1;
|
|
|
|
|
int x;
|
|
|
|
|
for (x = 0; x < 2; x++)
|
|
|
|
|
setpageptr(2, A + (x << 11), &PRGptr[r][((VA + x) & mask2) << 11], PRGram[r]);
|
|
|
|
|
} else {
|
|
|
|
|
setpageptr(2, A, NULL, PRGram[r]);
|
|
|
|
|
setpageptr(2, A + 0x800, NULL, PRGram[r]);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2014-03-30 22:15:17 +02:00
|
|
|
V &= PRGmask4[r];
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
setpageptr(4, A, &PRGptr[r][V << 12], PRGram[r]);
|
2014-03-30 22:15:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setprg4(uint32 A, uint32 V) {
|
|
|
|
|
setprg4r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setprg8r(int r, uint32 A, uint32 V) {
|
|
|
|
|
if (PRGsize[r] >= 8192) {
|
|
|
|
|
V &= PRGmask8[r];
|
|
|
|
|
setpageptr(8, A, PRGptr[r] ? (&PRGptr[r][V << 13]) : 0, PRGram[r]);
|
|
|
|
|
} else {
|
|
|
|
|
uint32 VA = V << 2;
|
|
|
|
|
int x;
|
|
|
|
|
for (x = 0; x < 4; x++)
|
|
|
|
|
setpageptr(2, A + (x << 11), PRGptr[r] ? (&PRGptr[r][((VA + x) & PRGmask2[r]) << 11]) : 0, PRGram[r]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setprg8(uint32 A, uint32 V) {
|
|
|
|
|
setprg8r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setprg16r(int r, uint32 A, uint32 V) {
|
|
|
|
|
if (PRGsize[r] >= 16384) {
|
|
|
|
|
V &= PRGmask16[r];
|
|
|
|
|
setpageptr(16, A, PRGptr[r] ? (&PRGptr[r][V << 14]) : 0, PRGram[r]);
|
|
|
|
|
} else {
|
|
|
|
|
uint32 VA = V << 3;
|
|
|
|
|
int x;
|
|
|
|
|
|
|
|
|
|
for (x = 0; x < 8; x++)
|
|
|
|
|
setpageptr(2, A + (x << 11), PRGptr[r] ? (&PRGptr[r][((VA + x) & PRGmask2[r]) << 11]) : 0, PRGram[r]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setprg16(uint32 A, uint32 V) {
|
|
|
|
|
setprg16r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setprg32r(int r, uint32 A, uint32 V) {
|
|
|
|
|
if (PRGsize[r] >= 32768) {
|
|
|
|
|
V &= PRGmask32[r];
|
|
|
|
|
setpageptr(32, A, PRGptr[r] ? (&PRGptr[r][V << 15]) : 0, PRGram[r]);
|
|
|
|
|
} else {
|
|
|
|
|
uint32 VA = V << 4;
|
|
|
|
|
int x;
|
|
|
|
|
|
|
|
|
|
for (x = 0; x < 16; x++)
|
|
|
|
|
setpageptr(2, A + (x << 11), PRGptr[r] ? (&PRGptr[r][((VA + x) & PRGmask2[r]) << 11]) : 0, PRGram[r]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setprg32(uint32 A, uint32 V) {
|
|
|
|
|
setprg32r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setchr1r(int r, uint32 A, uint32 V) {
|
|
|
|
|
if (!CHRptr[r]) return;
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
if (CHRsize[r] < 1024) return; /* mask underflow guard */
|
2014-03-30 22:15:17 +02:00
|
|
|
FCEUPPU_LineUpdate();
|
|
|
|
|
V &= CHRmask1[r];
|
|
|
|
|
if (CHRram[r])
|
|
|
|
|
PPUCHRRAM |= (1 << (A >> 10));
|
|
|
|
|
else
|
|
|
|
|
PPUCHRRAM &= ~(1 << (A >> 10));
|
|
|
|
|
VPageR[(A) >> 10] = &CHRptr[r][(V) << 10] - (A);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setchr2r(int r, uint32 A, uint32 V) {
|
|
|
|
|
if (!CHRptr[r]) return;
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
if (CHRsize[r] < 2048) return; /* mask underflow guard */
|
2014-03-30 22:15:17 +02:00
|
|
|
FCEUPPU_LineUpdate();
|
|
|
|
|
V &= CHRmask2[r];
|
|
|
|
|
VPageR[(A) >> 10] = VPageR[((A) >> 10) + 1] = &CHRptr[r][(V) << 11] - (A);
|
|
|
|
|
if (CHRram[r])
|
|
|
|
|
PPUCHRRAM |= (3 << (A >> 10));
|
|
|
|
|
else
|
|
|
|
|
PPUCHRRAM &= ~(3 << (A >> 10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setchr4r(int r, uint32 A, uint32 V) {
|
|
|
|
|
if (!CHRptr[r]) return;
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
if (CHRsize[r] < 4096) return; /* mask underflow guard */
|
2014-03-30 22:15:17 +02:00
|
|
|
FCEUPPU_LineUpdate();
|
|
|
|
|
V &= CHRmask4[r];
|
|
|
|
|
VPageR[(A) >> 10] = VPageR[((A) >> 10) + 1] =
|
|
|
|
|
VPageR[((A) >> 10) + 2] = VPageR[((A) >> 10) + 3] = &CHRptr[r][(V) << 12] - (A);
|
|
|
|
|
if (CHRram[r])
|
|
|
|
|
PPUCHRRAM |= (15 << (A >> 10));
|
|
|
|
|
else
|
|
|
|
|
PPUCHRRAM &= ~(15 << (A >> 10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setchr8r(int r, uint32 V) {
|
|
|
|
|
int x;
|
|
|
|
|
|
|
|
|
|
if (!CHRptr[r]) return;
|
|
|
|
|
FCEUPPU_LineUpdate();
|
core: memory-safety, leak, and savestate-portability audit fixes
Squashed series of ~50 distinct bugs found during a multi-day
security/correctness audit, ranging from ROM-triggerable heap
corruption and savestate-triggered OOB read primitives down to
obscure cross-platform savestate breakage.
Build is clean on `make platform=unix` with zero new warnings.
CRITICAL (ROM-triggerable, exploitable on the user's machine)
=============================================================
* ines.c iNES 2.0 PRG/CHR exponent overflow leading to undersized
allocation followed by heap buffer overflow on fread (pow() with
attacker-chosen exponent up to 63). Cap exponent at 30 and
compute size in uint32 with explicit cap below 2 GiB before
storing in int.
* ines.c miscROMSize int wraparound from attacker-controlled PRG/
CHR sizes; previous '& 0x8000000' check missed common underflow
cases. Compute in int64 and reject <= 0 or > 128 MiB.
* unif.c MAPR chunk OOB heap read on chunks of size < 4. Validate
chunk size before allocating board-name buffer.
* unif.c chunk size truncation: int conversion + missing cap allowed
a 4 GiB chunk size to wrap. Cap at 16 MiB.
* unif.c FixRomSize infinite loop on size > 0x80000000. Cap input.
* unif.c DINF chunk: months[(m - 1) % 12] is UB for m==0; m comes
from the .unf file. Clamp m into [1..12] before subtracting.
* unif.c NAME chunk: GameInfo->name = malloc(...) was unchecked.
* nsf.c size underflow: FCEU_fgetsize() - 0x80 wraps to ~UINT64_MAX
on tiny files, propagating a huge size into uppow2/FCEU_malloc.
Validate size > 0x80 before subtracting.
* nsf.c NSFMaxBank * 4096 cap tightened from 1<<20 (UB on signed-int
overflow) to 1<<19 (fits in int).
* general.c uppow2 had signed-int UB (1 << 32) and wrapped to 0 for
inputs near uint32 max. Cap at 0x80000000.
* cheat.c SubCheats[256] BSS overflow when more than 256 GG/PAR
substitution cheats are active. The array is adjacent to MMapPtrs[64]
in BSS which is exposed via retro_get_memory_data, making this
overflow visible from outside the core.
* libretro.c retro_cheat_set strcpy stack overflow with arbitrary-
length cheat strings from the frontend. Use strlcpy.
CRITICAL (savestate-triggerable on a malicious .fcs file)
=========================================================
* fds.c FDS savestate OOB read primitive: InDisk loaded from
savestate is used as index into 8-element diskdata[] static
pointer array. A value 8..254 dereferences arbitrary memory as
a pointer (heap-read primitive). Also bounds-checked SelectDisk,
mapperFDS_block, mapperFDS_blockstart, mapperFDS_diskaddr,
mapperFDS_blocklen so blockstart+diskaddr stays within the 65500-
byte disk buffer.
* 11 mappers had savestate-loaded variables masked at write time
but not at restore time, used as indices into fixed arrays:
- 88.c, KS7037.c, 112.c cmd indexes reg[8]
- sachen.c (S8259, S74LS374N) cmd indexes latch[8]
- 357.c dipswitch indexes outer_bank[4]
- unrom512.c flash_state indexes erase_a/d/b[5]
- 368.c preg indexes banks[8]
- 69.c sndcmd indexes sreg[14]
- mmc2and4.c latch0/latch1 index creg[4]
None individually exploitable into RCE - the array writes corrupt
adjacent BSS with constrained data flowing in - but each is an
out-of-bounds read or write from attacker-controllable input.
HIGH (memory-safety, reachable on any load)
===========================================
* fceu-memory.c FCEU_malloc deref-of-NULL on allocation failure
('ret = 0; memset(ret, 0, size);').
* file.c multiple FCEUFILE/MakeMemWrap/MakeMemWrapBuffer unchecked
allocations and unchecked filestream_tell return.
* libretro.c GameInfo NULL-derefs in three entry points
(retro_set_controller_port_device, retro_get_memory_data,
retro_get_memory_size) reachable on operations called before a
successful load.
* libretro.c framebuffer leak ~256 KB per failed FCEUI_LoadGame
(the libretro frontend doesn't call retro_unload_game on a
failed load).
* libretro.c 3DS retro_deinit unchecked linearFree.
* libretro.c stereo / NTSC filter unchecked malloc returns.
* fceu.c FCEUI_LoadGame unchecked GameInfo malloc.
* fds.c SubLoad and FDSLoad unchecked FCEU_malloc on diskdatao
backup buffers (NULL-deref in subsequent memcpy).
* fceu.c AllocGenieRW partial-failure leak: AReadG allocated but
BWriteG malloc fails -> 256 KB leak per retry.
* input/bworld.c Update(): unbounded strcpy from attacker-supplied
data into 20-byte bdata. Replaced with length-bounded copy.
* cart.c setprg2r/4r and setchr1r/2r/4r/8r mask-underflow guards.
SetupCartPRGMapping/SetupCartCHRMapping compute (size >> N) - 1
which underflows to 0xFFFFFFFF for chips smaller than the unit.
setprg8r/16r/32r and setchr8r already had defensive size checks;
the smaller units did not. malee.c (2 KB chip) and mapper 218
(2 KB NTARAM-as-CHR) accidentally avoid OOB; the primitive is
unsafe for any future board.
* video.c FCEU_InitVirtualVideo XBuf/XDBuf partial-failure leak.
* video.c, fceu.c FCEU_DispMessage / FCEU_printf / FCEU_PrintError:
vsprintf into fixed stack buffers replaced with vsnprintf.
* core: validate magic-string read length on FDS/UNIF/NSF load
(reject files too short to contain the magic so previous static
buffer / stack garbage doesn't spuriously match).
LEAKS (every cart load/unload cycle)
====================================
* mmc5.c MMC5 cart-side: WRAM (up to 64K) + MMC5fill (1K) + ExRAM
(1K) leaked per cycle. NSFMMC5_Close existed but was only used
for NSF code path.
* onebus.c Mapper 270/436: 8 KB CHRRAM leaked per cycle.
* 330.c, 375.c, 528.c: 8 KB WRAM each, no Close at all.
CORRECTNESS (UB / ABI / endianness)
===================================
* fceu-endian.c FlipByteOrder loop bound was 'count' instead of
'count/2', making it a no-op for every even count and silently
breaking savestate portability for every FCEUSTATE_RLSB-marked
field. The '#ifndef GEKKO' workarounds scattered through the
codebase (sound.c, vrc7.c, vrc6.c) were symptoms of this root
cause.
* state.c AddExState bounds check ran AFTER writing the entry, so
when SFEXINDEX hit the 63-entry cap the next call would write
the entry then immediately overwrite it with the terminator.
* state.c FCEUSS_Save_Mem: post-save callback was guarded by
'if (SPreSave)' instead of 'if (SPostSave)'.
* Sequence-point UB in counter expressions across 4 board files,
e.g. 'x = !x ? a : --x' and 'x = ++x % n'. GCC -Wsequence-point
flags all five sites (285.c, 413.c, asic_mmc3.c, jyasic.c).
* SFORMAT size mismatches between declared variable type and
AddExState size argument:
- asic_vrc3.c VRC3_count/VRC3_reload (uint16, saved as 1 byte)
- mmc3.c m555_count (uint32, saved as 2 bytes), m555_count_-
expired (uint8, saved as 2 bytes - OOB write into adjacent
BSS).
* unrom512.c UNROM-512 mapper 30 .srm save format: host-endian
uint32 flash write counters. Now stored as LE on disk regardless
of host (Battle Kid 2, Twin Dragons, Lizard, Sole, etc.).
* ~70 multi-byte single-variable SFORMAT/AddExState entries across
36 board files were saved as host-byte-order. After the
FlipByteOrder fix, these are now byte-swapped on BE so cross-
platform savestates round-trip correctly.
* coolgirl.c new ExStateLE() macro added; 22 multi-byte sites.
* pic16c5x.c 11 multi-byte AddExState calls (m_PC, m_PREVPC,
m_CONFIG, m_WDT, m_prescaler, m_opcode, m_STACK[0/1], m_icount,
m_delay_timer, m_rtcc, m_inst_cycles, m_clock2cycle).
* onebus.c PowerJoy Supermax submapper detection used non-portable
*(uint32*)&info->MD5 cast that read different bytes on LE vs BE.
* fds.c clean up redundant FCEUSTATE_RLSB encoding in AddExState
calls that also passed type=1 (idempotent OR; readability fix).
DOCUMENTATION
=============
* input.c UpdateGP's *(uint32*)data cast looks like a typical
endian bug but is actually correct (the libretro frontend builds
JSReturn with matching host-uint32 shifts). Comment added to
prevent future "fixes" from breaking it.
Limitations not addressed
=========================
* Element-stride-aware byte swapping. The savestate byte-swap
mechanism (FlipByteOrder over the entire SFORMAT entry buffer)
is structurally wrong for arrays of multi-byte values: it
reverses the whole buffer end-to-end instead of byte-swapping
each element. Several places that need cross-platform-portable
arrays (VRC7 sound state, jyasic chr[8]) work around this by
either splitting arrays into per-element SFORMAT entries (n106
PlayIndex, bandai reg) or by skipping save entirely on BE via
#ifndef GEKKO. A proper fix would extend the size encoding with
an element-stride field. Left for a future change because it
would change the savestate format.
* Strict-aliasing UB in ppu.c (around 9 sites doing
*(uint32*)uint8_buf for fast 4-byte writes via FCEU_dwmemset).
Works in practice with all common compilers because the patterns
are byte-symmetric, but is formally UB.
* FCEU_gmalloc calls exit(1) on OOM. A libretro core should never
exit() because that takes down the whole frontend. Used by
100+ call sites; refactoring to return-NULL is out of scope here.
Testing
=======
* Build: clean on `make platform=unix` with -O2; no new warnings.
* FlipByteOrder fix verified by hand-trace and a standalone unit
test for counts 2, 4, 8.
* uppow2 fix verified by unit test across 13 boundary cases.
* SFORMAT size mismatches and missing-RLSB cases identified by
Python static-analysis scripts that cross-reference SFORMAT
entries against variable declarations.
* iNES 2.0 exponent fix verified by hand-tracing what byte 0xFF
produces post-fix: exp=30 (capped), mult=7, size=3 GiB nominal,
capped to 1 GiB, capped to 2 GiB by uppow2, FCEU_malloc returns
NULL on most systems, loader returns 0. No heap overflow for
any input byte.
* Savestate-loaded array index audit: built a Python scanner that
extracts each AddExState/SFORMAT entry and cross-references the
variable name against array-index uses in the same file. All
flagged sites covered.
* A libFuzzer harness and seed corpus generator (fuzz_main.c,
gen_seed_corpus.py) accompany this submission for ongoing
regression testing.
2026-05-04 02:15:40 +02:00
|
|
|
if (CHRsize[r] < 8192) {
|
|
|
|
|
/* Undersized chip: clamp V to 0 to avoid OOB indexing. The 2KB
|
|
|
|
|
* NTARAM-as-CHR pattern in mapper 218 currently relies on this
|
|
|
|
|
* (always called with V=0); treat anything else as a logic
|
|
|
|
|
* error and clamp. */
|
|
|
|
|
V = 0;
|
|
|
|
|
} else {
|
|
|
|
|
V &= CHRmask8[r];
|
|
|
|
|
}
|
2014-03-30 22:15:17 +02:00
|
|
|
for (x = 7; x >= 0; x--)
|
|
|
|
|
VPageR[x] = &CHRptr[r][V << 13];
|
|
|
|
|
if (CHRram[r])
|
|
|
|
|
PPUCHRRAM |= (255);
|
|
|
|
|
else
|
2017-10-18 10:50:42 +08:00
|
|
|
PPUCHRRAM = 0;
|
2014-03-30 22:15:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setchr1(uint32 A, uint32 V) {
|
|
|
|
|
setchr1r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setchr2(uint32 A, uint32 V) {
|
|
|
|
|
setchr2r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(2) setchr4(uint32 A, uint32 V) {
|
|
|
|
|
setchr4r(0, A, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(1) setchr8(uint32 V) {
|
|
|
|
|
setchr8r(0, V);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* This function can be called without calling SetupCartMirroring(). */
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(3) setntamem(uint8 * p, int ram, uint32 b) {
|
|
|
|
|
FCEUPPU_LineUpdate();
|
|
|
|
|
vnapage[b] = p;
|
|
|
|
|
PPUNTARAM &= ~(1 << b);
|
|
|
|
|
if (ram)
|
|
|
|
|
PPUNTARAM |= 1 << b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int mirrorhard = 0;
|
|
|
|
|
void setmirrorw(int a, int b, int c, int d) {
|
|
|
|
|
FCEUPPU_LineUpdate();
|
|
|
|
|
vnapage[0] = NTARAM + a * 0x400;
|
|
|
|
|
vnapage[1] = NTARAM + b * 0x400;
|
|
|
|
|
vnapage[2] = NTARAM + c * 0x400;
|
|
|
|
|
vnapage[3] = NTARAM + d * 0x400;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FASTAPASS(1) setmirror(int t) {
|
|
|
|
|
FCEUPPU_LineUpdate();
|
|
|
|
|
if (!mirrorhard) {
|
|
|
|
|
switch (t) {
|
|
|
|
|
case MI_H:
|
|
|
|
|
vnapage[0] = vnapage[1] = NTARAM; vnapage[2] = vnapage[3] = NTARAM + 0x400;
|
|
|
|
|
break;
|
|
|
|
|
case MI_V:
|
|
|
|
|
vnapage[0] = vnapage[2] = NTARAM; vnapage[1] = vnapage[3] = NTARAM + 0x400;
|
|
|
|
|
break;
|
|
|
|
|
case MI_0:
|
|
|
|
|
vnapage[0] = vnapage[1] = vnapage[2] = vnapage[3] = NTARAM;
|
|
|
|
|
break;
|
|
|
|
|
case MI_1:
|
|
|
|
|
vnapage[0] = vnapage[1] = vnapage[2] = vnapage[3] = NTARAM + 0x400;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
PPUNTARAM = 0xF;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetupCartMirroring(int m, int hard, uint8 *extra) {
|
|
|
|
|
if (m < 4) {
|
|
|
|
|
mirrorhard = 0;
|
|
|
|
|
setmirror(m);
|
|
|
|
|
} else {
|
|
|
|
|
vnapage[0] = NTARAM;
|
|
|
|
|
vnapage[1] = NTARAM + 0x400;
|
|
|
|
|
vnapage[2] = extra;
|
|
|
|
|
vnapage[3] = extra + 0x400;
|
|
|
|
|
PPUNTARAM = 0xF;
|
|
|
|
|
}
|
|
|
|
|
mirrorhard = hard;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint8 *GENIEROM = 0;
|
|
|
|
|
|
|
|
|
|
void FixGenieMap(void);
|
|
|
|
|
|
|
|
|
|
/* Called when a game(file) is opened successfully. */
|
|
|
|
|
void FCEU_OpenGenie(void) {
|
2021-10-20 16:31:45 +01:00
|
|
|
RFILE *fp = NULL;
|
2014-03-30 22:15:17 +02:00
|
|
|
int x;
|
|
|
|
|
|
|
|
|
|
if (!GENIEROM) {
|
|
|
|
|
char *fn;
|
|
|
|
|
|
|
|
|
|
if (!(GENIEROM = (uint8*)FCEU_malloc(4096 + 1024))) return;
|
|
|
|
|
|
|
|
|
|
fn = FCEU_MakeFName(FCEUMKF_GGROM, 0, 0);
|
2021-10-20 16:31:45 +01:00
|
|
|
|
2021-10-21 15:52:40 +01:00
|
|
|
if (!string_is_empty(fn) && path_is_valid(fn))
|
2021-10-20 16:31:45 +01:00
|
|
|
fp = filestream_open(fn,
|
|
|
|
|
RETRO_VFS_FILE_ACCESS_READ,
|
|
|
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
|
|
|
|
2021-10-21 15:52:40 +01:00
|
|
|
free(fn);
|
|
|
|
|
fn = NULL;
|
|
|
|
|
|
2014-03-30 22:15:17 +02:00
|
|
|
if (!fp) {
|
2021-10-21 15:52:40 +01:00
|
|
|
FCEU_PrintError("Error opening Game Genie ROM image!\n");
|
|
|
|
|
FCEUD_DispMessage(RETRO_LOG_WARN, 3000, "Game Genie ROM image (gamegenie.nes) missing");
|
2014-03-30 22:15:17 +02:00
|
|
|
free(GENIEROM);
|
|
|
|
|
GENIEROM = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-10-20 16:31:45 +01:00
|
|
|
if (filestream_read(fp, GENIEROM, 16) != 16) {
|
2014-03-30 22:15:17 +02:00
|
|
|
grerr:
|
2021-10-21 15:52:40 +01:00
|
|
|
FCEU_PrintError("Error reading from Game Genie ROM image!\n");
|
|
|
|
|
FCEUD_DispMessage(RETRO_LOG_WARN, 3000, "Failed to read Game Genie ROM image (gamegenie.nes)");
|
2014-03-30 22:15:17 +02:00
|
|
|
free(GENIEROM);
|
|
|
|
|
GENIEROM = 0;
|
2021-10-20 16:31:45 +01:00
|
|
|
filestream_close(fp);
|
2014-03-30 22:15:17 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (GENIEROM[0] == 0x4E) { /* iNES ROM image */
|
2021-10-20 16:31:45 +01:00
|
|
|
if (filestream_read(fp, GENIEROM, 4096) != 4096)
|
2014-03-30 22:15:17 +02:00
|
|
|
goto grerr;
|
2021-10-20 16:31:45 +01:00
|
|
|
if (filestream_seek(fp, 16384 - 4096, RETRO_VFS_SEEK_POSITION_CURRENT))
|
2014-03-30 22:15:17 +02:00
|
|
|
goto grerr;
|
2021-10-20 16:31:45 +01:00
|
|
|
if (filestream_read(fp, GENIEROM + 4096, 256) != 256)
|
2014-03-30 22:15:17 +02:00
|
|
|
goto grerr;
|
|
|
|
|
} else {
|
2021-10-20 16:31:45 +01:00
|
|
|
if (filestream_read(fp, GENIEROM + 16, 4352 - 16) != (4352 - 16))
|
2014-03-30 22:15:17 +02:00
|
|
|
goto grerr;
|
|
|
|
|
}
|
2021-10-20 16:31:45 +01:00
|
|
|
filestream_close(fp);
|
2014-03-30 22:15:17 +02:00
|
|
|
|
|
|
|
|
/* Workaround for the FCE Ultra CHR page size only being 1KB */
|
|
|
|
|
for (x = 0; x < 4; x++)
|
|
|
|
|
memcpy(GENIEROM + 4096 + (x << 8), GENIEROM + 4096, 256);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
geniestage = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Called when a game is closed. */
|
|
|
|
|
void FCEU_CloseGenie(void) {
|
|
|
|
|
/* No good reason to free() the Game Genie ROM image data. */
|
|
|
|
|
geniestage = 0;
|
|
|
|
|
FlushGenieRW();
|
|
|
|
|
VPageR = VPage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FCEU_KillGenie(void) {
|
|
|
|
|
if (GENIEROM) {
|
|
|
|
|
free(GENIEROM);
|
|
|
|
|
GENIEROM = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DECLFR(GenieRead) {
|
|
|
|
|
return GENIEROM[A & 4095];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DECLFW(GenieWrite) {
|
|
|
|
|
switch (A) {
|
|
|
|
|
case 0x800c:
|
|
|
|
|
case 0x8008:
|
|
|
|
|
case 0x8004: genieval[((A - 4) & 0xF) >> 2] = V; break;
|
|
|
|
|
|
|
|
|
|
case 0x800b:
|
|
|
|
|
case 0x8007:
|
|
|
|
|
case 0x8003: geniech[((A - 3) & 0xF) >> 2] = V; break;
|
|
|
|
|
|
|
|
|
|
case 0x800a:
|
|
|
|
|
case 0x8006:
|
|
|
|
|
case 0x8002: genieaddr[((A - 2) & 0xF) >> 2] &= 0xFF00; genieaddr[((A - 2) & 0xF) >> 2] |= V; break;
|
|
|
|
|
|
|
|
|
|
case 0x8009:
|
|
|
|
|
case 0x8005:
|
|
|
|
|
case 0x8001: genieaddr[((A - 1) & 0xF) >> 2] &= 0xFF; genieaddr[((A - 1) & 0xF) >> 2] |= (V | 0x80) << 8; break;
|
|
|
|
|
|
|
|
|
|
case 0x8000:
|
|
|
|
|
if (!V)
|
|
|
|
|
FixGenieMap();
|
|
|
|
|
else {
|
|
|
|
|
modcon = V ^ 0xFF;
|
|
|
|
|
if (V == 0x71)
|
|
|
|
|
modcon = 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static readfunc GenieBackup[3];
|
|
|
|
|
|
|
|
|
|
static DECLFR(GenieFix1) {
|
|
|
|
|
uint8 r = GenieBackup[0](A);
|
|
|
|
|
|
2017-10-15 03:13:11 +08:00
|
|
|
if ((modcon >> 1) & 1) /* No check */
|
2014-03-30 22:15:17 +02:00
|
|
|
return genieval[0];
|
|
|
|
|
else if (r == geniech[0])
|
|
|
|
|
return genieval[0];
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DECLFR(GenieFix2) {
|
|
|
|
|
uint8 r = GenieBackup[1](A);
|
|
|
|
|
|
2017-10-15 03:13:11 +08:00
|
|
|
if ((modcon >> 2) & 1) /* No check */
|
2014-03-30 22:15:17 +02:00
|
|
|
return genieval[1];
|
|
|
|
|
else if (r == geniech[1])
|
|
|
|
|
return genieval[1];
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DECLFR(GenieFix3) {
|
|
|
|
|
uint8 r = GenieBackup[2](A);
|
|
|
|
|
|
2017-10-15 03:13:11 +08:00
|
|
|
if ((modcon >> 3) & 1) /* No check */
|
2014-03-30 22:15:17 +02:00
|
|
|
return genieval[2];
|
|
|
|
|
else if (r == geniech[2])
|
|
|
|
|
return genieval[2];
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void FixGenieMap(void) {
|
|
|
|
|
int x;
|
|
|
|
|
|
|
|
|
|
geniestage = 2;
|
|
|
|
|
|
|
|
|
|
for (x = 0; x < 8; x++)
|
|
|
|
|
VPage[x] = VPageG[x];
|
|
|
|
|
|
|
|
|
|
VPageR = VPage;
|
|
|
|
|
FlushGenieRW();
|
|
|
|
|
for (x = 0; x < 3; x++)
|
|
|
|
|
if ((modcon >> (4 + x)) & 1) {
|
|
|
|
|
readfunc tmp[3] = { GenieFix1, GenieFix2, GenieFix3 };
|
|
|
|
|
GenieBackup[x] = GetReadHandler(genieaddr[x]);
|
|
|
|
|
SetReadHandler(genieaddr[x], genieaddr[x], tmp[x]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FCEU_GeniePower(void) {
|
|
|
|
|
uint32 x;
|
|
|
|
|
|
|
|
|
|
if (!geniestage)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
geniestage = 1;
|
|
|
|
|
for (x = 0; x < 3; x++) {
|
|
|
|
|
genieval[x] = 0xFF;
|
|
|
|
|
geniech[x] = 0xFF;
|
|
|
|
|
genieaddr[x] = 0xFFFF;
|
|
|
|
|
}
|
|
|
|
|
modcon = 0;
|
|
|
|
|
|
|
|
|
|
SetWriteHandler(0x8000, 0xFFFF, GenieWrite);
|
|
|
|
|
SetReadHandler(0x8000, 0xFFFF, GenieRead);
|
|
|
|
|
|
|
|
|
|
for (x = 0; x < 8; x++)
|
|
|
|
|
VPage[x] = GENIEROM + 4096 - 0x400 * x;
|
|
|
|
|
|
|
|
|
|
if (AllocGenieRW())
|
|
|
|
|
VPageR = VPageG;
|
|
|
|
|
else
|
|
|
|
|
geniestage = 2;
|
|
|
|
|
}
|