From 1185db89c15e738ca19874e675154c92be75f370 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 4 May 2026 02:15:40 +0200 Subject: [PATCH] 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. --- src/boards/09-034a.c | 4 +- src/boards/106.c | 2 +- src/boards/112.c | 1 + src/boards/128.c | 2 +- src/boards/18.c | 4 +- src/boards/228.c | 2 +- src/boards/285.c | 2 +- src/boards/33.c | 4 +- src/boards/330.c | 9 ++++- src/boards/354.c | 2 +- src/boards/355.c | 2 +- src/boards/357.c | 1 + src/boards/368.c | 1 + src/boards/375.c | 9 ++++- src/boards/40.c | 4 +- src/boards/413.c | 7 +++- src/boards/414.c | 2 +- src/boards/416.c | 2 +- src/boards/417.c | 2 +- src/boards/42.c | 4 +- src/boards/43.c | 4 +- src/boards/434.c | 2 +- src/boards/465.c | 2 +- src/boards/50.c | 4 +- src/boards/528.c | 7 ++++ src/boards/62.c | 2 +- src/boards/65.c | 4 +- src/boards/67.c | 4 +- src/boards/68.c | 2 +- src/boards/69.c | 3 +- src/boards/80.c | 4 +- src/boards/88.c | 1 + src/boards/KS7017.c | 6 +-- src/boards/KS7037.c | 1 + src/boards/__dummy_mapper.c | 4 +- src/boards/addrlatch.c | 2 +- src/boards/asic_latch.c | 2 +- src/boards/asic_mmc3.c | 5 ++- src/boards/asic_vrc3.c | 4 +- src/boards/bj56.c | 2 +- src/boards/coolgirl.c | 49 ++++++++++++----------- src/boards/jyasic.c | 16 +++++--- src/boards/lh53.c | 2 +- src/boards/mihunche.c | 2 +- src/boards/mmc2and4.c | 5 +++ src/boards/mmc3.c | 4 +- src/boards/mmc5.c | 15 +++++++ src/boards/onebus.c | 26 ++++++++++--- src/boards/pec-586.c | 2 +- src/boards/pic16c5x.c | 26 ++++++------- src/boards/sachen.c | 2 + src/boards/unrom512.c | 44 ++++++++++++++++++--- src/boards/vrc3.c | 4 +- src/boards/vrc6.c | 4 +- src/boards/vrc7.c | 4 +- src/boards/vrc7p.c | 4 +- src/boards/vrcirq.c | 2 +- src/cart.c | 42 ++++++++++++++++++-- src/cheat.c | 10 ++++- src/drivers/libretro/libretro.c | 49 +++++++++++++++++++---- src/fceu-endian.c | 9 ++++- src/fceu-memory.c | 2 +- src/fceu.c | 17 +++++++- src/fds.c | 69 +++++++++++++++++++++++++++++---- src/file.c | 35 ++++++++++++++--- src/general.c | 16 ++++++-- src/ines.c | 48 +++++++++++++++++++++-- src/input.c | 4 ++ src/input/bworld.c | 11 +++++- src/nsf.c | 31 ++++++++++++++- src/state.c | 21 ++++++++-- src/unif.c | 43 +++++++++++++++++--- src/video.c | 11 +++++- 73 files changed, 595 insertions(+), 165 deletions(-) diff --git a/src/boards/09-034a.c b/src/boards/09-034a.c index 12e5b79..72ee4e6 100644 --- a/src/boards/09-034a.c +++ b/src/boards/09-034a.c @@ -37,8 +37,8 @@ static uint32 IRQCount, IRQa; static SFORMAT StateRegs[] = { - { &IRQCount, 4, "IRQC" }, - { &IRQa, 4, "IRQA" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQa, 4 | FCEUSTATE_RLSB, "IRQA" }, { &prg, 1, "PRG" }, { 0 } }; diff --git a/src/boards/106.c b/src/boards/106.c index f471ecb..3945ed7 100644 --- a/src/boards/106.c +++ b/src/boards/106.c @@ -28,7 +28,7 @@ static uint32 WRAMSIZE; static SFORMAT StateRegs[] = { { &IRQa, 1, "IRQA" }, - { &IRQCount, 4, "IRQC" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, { reg, 16, "REGS" }, { 0 } }; diff --git a/src/boards/112.c b/src/boards/112.c index a95cb5b..a469719 100644 --- a/src/boards/112.c +++ b/src/boards/112.c @@ -76,6 +76,7 @@ static void M112Power(void) { } static void StateRestore(int version) { + cmd &= 7; /* reg[] has 8 entries */ Sync(); } diff --git a/src/boards/128.c b/src/boards/128.c index 83bb900..0aff661 100644 --- a/src/boards/128.c +++ b/src/boards/128.c @@ -52,5 +52,5 @@ void Mapper128_Init(CartInfo *info) { Latch_Init(info, Sync, NULL, 0, 0); info->Power = M128Power; info->Reset = M128Reset; - AddExState(&outerbank, 2, 0, "BANK"); + AddExState(&outerbank, 2, 1, "BANK"); } diff --git a/src/boards/18.c b/src/boards/18.c index 49f4675..da92770 100644 --- a/src/boards/18.c +++ b/src/boards/18.c @@ -32,8 +32,8 @@ static SFORMAT StateRegs[] = { creg, 8, "CREG" }, { &mirr, 1, "MIRR" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 4, "IRQC" }, - { &IRQLatch, 4, "IRQL" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQLatch, 4 | FCEUSTATE_RLSB, "IRQL" }, { 0 } }; diff --git a/src/boards/228.c b/src/boards/228.c index af23d24..81201a0 100644 --- a/src/boards/228.c +++ b/src/boards/228.c @@ -26,7 +26,7 @@ static uint16 areg; static SFORMAT StateRegs[] = { { mram, 4, "MRAM" }, - { &areg, 2, "AREG" }, + { &areg, 2 | FCEUSTATE_RLSB, "AREG" }, { &vreg, 1, "VREG" }, { 0 } }; diff --git a/src/boards/285.c b/src/boards/285.c index 8a10f35..2b1c72a 100644 --- a/src/boards/285.c +++ b/src/boards/285.c @@ -66,7 +66,7 @@ static void power () { } static void reset () { - pad = ++pad %24; + pad = (pad + 1) % 24; Latch_clear(); } diff --git a/src/boards/33.c b/src/boards/33.c index 2838100..6f200c9 100644 --- a/src/boards/33.c +++ b/src/boards/33.c @@ -30,8 +30,8 @@ static SFORMAT StateRegs[] = { regs, 8, "PREG" }, { &mirr, 1, "MIRR" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 2, "IRQC" }, - { &IRQLatch, 2, "IRQL" }, + { &IRQCount, 2 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQLatch, 2 | FCEUSTATE_RLSB, "IRQL" }, { 0 } }; diff --git a/src/boards/330.c b/src/boards/330.c index 4a3055a..b2ab32b 100644 --- a/src/boards/330.c +++ b/src/boards/330.c @@ -36,7 +36,7 @@ static SFORMAT StateRegs[] = { CHR, 8, "CHR" }, { NTAPage, 4, "NT" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 2, "IRQC" }, + { &IRQCount, 2 | FCEUSTATE_RLSB, "IRQC" }, { 0 } }; @@ -127,8 +127,15 @@ static void StateRestore(int version) { FixNTAR(); } +static void M330Close(void) { + if (WRAM) + FCEU_gfree(WRAM); + WRAM = NULL; +} + void Mapper330_Init(CartInfo *info) { info->Power = M330Power; + info->Close = M330Close; MapIRQHook = M330IRQHook; GameStateRestore = StateRestore; AddExState(&StateRegs, ~0, 0, 0); diff --git a/src/boards/354.c b/src/boards/354.c index df82ce8..b8bda59 100644 --- a/src/boards/354.c +++ b/src/boards/354.c @@ -26,7 +26,7 @@ static uint8 submapper; static SFORMAT StateRegs[] = { - { &latchAddr, 2, "ADDR" }, + { &latchAddr, 2 | FCEUSTATE_RLSB, "ADDR" }, { &latchData, 1, "DATA" }, { 0 } }; diff --git a/src/boards/355.c b/src/boards/355.c index 5f2c659..ae93b54 100644 --- a/src/boards/355.c +++ b/src/boards/355.c @@ -117,5 +117,5 @@ void UNL3DBlock_Init(CartInfo *info) { info->Reset = M355Reset; info->Close = M355Close; MapIRQHook = M355CPUIRQHook; - AddExState(&address, sizeof(address), 0, "ADDR"); + AddExState(&address, sizeof(address), 1, "ADDR"); } diff --git a/src/boards/357.c b/src/boards/357.c index 48088fc..a260008 100644 --- a/src/boards/357.c +++ b/src/boards/357.c @@ -113,6 +113,7 @@ static void FP_FASTAPASS(1) M357IRQHook(int a) { } static void StateRestore(int version) { + dipswitch &= 3; /* outer_bank[] has 4 entries */ Sync(); } diff --git a/src/boards/368.c b/src/boards/368.c index a8abf3e..db08973 100644 --- a/src/boards/368.c +++ b/src/boards/368.c @@ -100,6 +100,7 @@ static void FP_FASTAPASS(1) M368IRQHook(int a) { } static void StateRestore(int version) { + preg &= 7; /* banks[] has 8 entries */ Sync(); } diff --git a/src/boards/375.c b/src/boards/375.c index 5004fb2..11fc843 100644 --- a/src/boards/375.c +++ b/src/boards/375.c @@ -107,11 +107,18 @@ static void StateRestore(int version) { Sync(); } +static void M375Close(void) { + if (WRAM) + FCEU_gfree(WRAM); + WRAM = NULL; +} + void Mapper375_Init(CartInfo *info) { info->Power = M375Power; info->Reset = M375Reset; + info->Close = M375Close; GameStateRestore = StateRestore; - AddExState(&addrlatch, 2, 0, "ADDR"); + AddExState(&addrlatch, 2, 1, "ADDR"); AddExState(&datalatch, 1, 0, "DATA"); WRAMSIZE = 8192; diff --git a/src/boards/40.c b/src/boards/40.c index e64c16d..fe16148 100644 --- a/src/boards/40.c +++ b/src/boards/40.c @@ -30,8 +30,8 @@ static uint32 IRQCount, IRQa; static SFORMAT StateRegs[] = { - { &IRQCount, 4, "IRQC" }, - { &IRQa, 4, "IRQA" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQa, 4 | FCEUSTATE_RLSB, "IRQA" }, { ®, 1, "REG" }, { ®, 1, "OUTE" }, { 0 } diff --git a/src/boards/413.c b/src/boards/413.c index b2d0793..2faa38c 100644 --- a/src/boards/413.c +++ b/src/boards/413.c @@ -31,7 +31,7 @@ extern uint32 timestamp; static SFORMAT stateRegs[] ={ { reg, 4, "REGS" }, - { &pcmAddress, 4, "PCMA" }, + { &pcmAddress, 4 | FCEUSTATE_RLSB, "PCMA" }, { &pcmControl, 1, "PCMC" }, { &scanlineCounter, 1, "SLCN" }, { &scanlineReload, 1, "SLRL" }, @@ -82,7 +82,10 @@ static DECLFW(writeReg) { } static void horizontalBlanking () { - scanlineCounter =!scanlineCounter? scanlineReload: --scanlineCounter; + if (!scanlineCounter) + scanlineCounter = scanlineReload; + else + scanlineCounter--; if (!scanlineCounter && scanlineIRQ) X6502_IRQBegin(FCEU_IQEXT); } diff --git a/src/boards/414.c b/src/boards/414.c index e57e642..a66da80 100644 --- a/src/boards/414.c +++ b/src/boards/414.c @@ -26,7 +26,7 @@ static uint8 dipswitch; static SFORMAT StateRegs[] = { - { &latch_addr, 4, "ADDR" }, + { &latch_addr, 4 | FCEUSTATE_RLSB, "ADDR" }, { &latch_data, 1, "DATA" }, { &dipswitch, 1, "DIPS" }, { 0 } diff --git a/src/boards/416.c b/src/boards/416.c index ba0eb2c..9ea10b4 100644 --- a/src/boards/416.c +++ b/src/boards/416.c @@ -114,5 +114,5 @@ void Mapper416_Init(CartInfo *info) { AddExState(®, 1, 0, "REGS"); AddExState(&smb2j_reg, 1, 0, "SMBJ"); AddExState(&IRQa, 1, 0, "IRQa"); - AddExState(&IRQCount, 2, 0, "IRQC"); + AddExState(&IRQCount, 2, 1, "IRQC"); } diff --git a/src/boards/417.c b/src/boards/417.c index df5718f..26dfea8 100644 --- a/src/boards/417.c +++ b/src/boards/417.c @@ -32,7 +32,7 @@ static SFORMAT StateRegs[] = { { creg, 8, "CREG" }, { nt, 4, "NREG" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 2, "IRQC" }, + { &IRQCount, 2 | FCEUSTATE_RLSB, "IRQC" }, { 0 } }; diff --git a/src/boards/42.c b/src/boards/42.c index b507c34..25e9b6d 100644 --- a/src/boards/42.c +++ b/src/boards/42.c @@ -33,8 +33,8 @@ static SFORMAT StateRegs[] = { &preg, 1, "PREG" }, { &creg, 1, "CREG" }, { &mirr, 1, "MIRR" }, - { &IRQCount, 4, "IRQC" }, - { &IRQa, 4, "IRQA" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQa, 4 | FCEUSTATE_RLSB, "IRQA" }, { 0 } }; diff --git a/src/boards/43.c b/src/boards/43.c index 7a83108..d4c59cc 100644 --- a/src/boards/43.c +++ b/src/boards/43.c @@ -28,8 +28,8 @@ static uint32 IRQCount, IRQa; static SFORMAT StateRegs[] = { - { &IRQCount, 4, "IRQC" }, - { &IRQa, 4, "IRQA" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQa, 4 | FCEUSTATE_RLSB, "IRQA" }, { ®, 1, "REG" }, { &swap, 1, "SWAP" }, { 0 } diff --git a/src/boards/434.c b/src/boards/434.c index cbe0b45..eda4790 100644 --- a/src/boards/434.c +++ b/src/boards/434.c @@ -63,5 +63,5 @@ void Mapper434_Init(CartInfo *info) { info->Reset = Mapper434_Reset; info->Power = Mapper434_Power; GameStateRestore = StateRestore; - AddExState(&latch, 2, 0, "LATC"); + AddExState(&latch, 2, 1, "LATC"); } diff --git a/src/boards/465.c b/src/boards/465.c index e847a9b..ef5164b 100644 --- a/src/boards/465.c +++ b/src/boards/465.c @@ -25,7 +25,7 @@ static uint8 latchData; static SFORMAT StateRegs[] = { - { &latchAddr, 2, "ADDR" }, + { &latchAddr, 2 | FCEUSTATE_RLSB, "ADDR" }, { &latchData, 1, "DATA" }, { 0 } }; diff --git a/src/boards/50.c b/src/boards/50.c index d13dfdc..74d3e2f 100644 --- a/src/boards/50.c +++ b/src/boards/50.c @@ -28,8 +28,8 @@ static uint32 IRQCount, IRQa; static SFORMAT StateRegs[] = { - { &IRQCount, 4, "IRQC" }, - { &IRQa, 4, "IRQA" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQa, 4 | FCEUSTATE_RLSB, "IRQA" }, { ®, 1, "REG" }, { 0 } }; diff --git a/src/boards/528.c b/src/boards/528.c index e9e6dc1..7366dc6 100644 --- a/src/boards/528.c +++ b/src/boards/528.c @@ -114,8 +114,15 @@ static void StateRestore(int version) { Sync(); } +static void M528Close(void) { + if (WRAM) + FCEU_gfree(WRAM); + WRAM = NULL; +} + void Mapper528_Init(CartInfo *info) { info->Power = M528Power; + info->Close = M528Close; GameStateRestore = StateRestore; VRCIRQ_Init(); AddExState(&StateRegs, ~0, 0, 0); diff --git a/src/boards/62.c b/src/boards/62.c index 15edc02..c5032c5 100644 --- a/src/boards/62.c +++ b/src/boards/62.c @@ -25,7 +25,7 @@ static uint16 mode; static SFORMAT StateRegs[] = { { &bank, 1, "BANK" }, - { &mode, 2, "MODE" }, + { &mode, 2 | FCEUSTATE_RLSB, "MODE" }, { 0 } }; diff --git a/src/boards/65.c b/src/boards/65.c index cfe8d6c..c9503e7 100644 --- a/src/boards/65.c +++ b/src/boards/65.c @@ -30,8 +30,8 @@ static SFORMAT StateRegs[] = { creg, 8, "CREG" }, { &mirr, 1, "MIRR" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 2, "IRQC" }, - { &IRQLatch, 2, "IRQL" }, + { &IRQCount, 2 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQLatch, 2 | FCEUSTATE_RLSB, "IRQL" }, { 0 } }; diff --git a/src/boards/67.c b/src/boards/67.c index 6d8e5ab..1272d45 100644 --- a/src/boards/67.c +++ b/src/boards/67.c @@ -31,8 +31,8 @@ static SFORMAT StateRegs[] = { creg, 4, "CREG" }, { &mirr, 1, "MIRR" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 2, "IRQC" }, - { &IRQLatch, 2, "IRQL" }, + { &IRQCount, 2 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQLatch, 2 | FCEUSTATE_RLSB, "IRQL" }, { 0 } }; diff --git a/src/boards/68.c b/src/boards/68.c index 2e0ec8f..e883fd9 100644 --- a/src/boards/68.c +++ b/src/boards/68.c @@ -33,7 +33,7 @@ static SFORMAT StateRegs[] = { &mirr, 1, "MIRR" }, { &prg_reg, 1, "PRG" }, { &kogame, 1, "KGME" }, - { &count, 4, "CNT" }, + { &count, 4 | FCEUSTATE_RLSB, "CNT" }, { chr_reg, 4, "CHR" }, { 0 } }; diff --git a/src/boards/69.c b/src/boards/69.c index 3117106..c7d5231 100644 --- a/src/boards/69.c +++ b/src/boards/69.c @@ -36,7 +36,7 @@ static SFORMAT StateRegs[] = { creg, 8, "CREG" }, { &mirr, 1, "MIRR" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 4, "IRQC" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, { 0 } }; @@ -300,6 +300,7 @@ static void M69IRQHook(int a) { } static void StateRestore(int version) { + if (sndcmd >= 14) sndcmd = 0; /* sreg[] has 14 entries */ Sync(); } diff --git a/src/boards/80.c b/src/boards/80.c index 3427d83..3a02215 100644 --- a/src/boards/80.c +++ b/src/boards/80.c @@ -42,7 +42,7 @@ static SFORMAT StateRegs95[] = { preg, 3, "PREG" }, { creg, 6, "CREG" }, { mcache, 8, "MCCH" }, - { &lastppu, 4, "LPPU" }, + { &lastppu, 4 | FCEUSTATE_RLSB, "LPPU" }, { 0 } }; @@ -51,7 +51,7 @@ static SFORMAT StateRegs207[] = { preg, 3, "PREG" }, { creg, 6, "CREG" }, { mcache, 8, "MCCH" }, - { &lastppu, 4, "LPPU" }, + { &lastppu, 4 | FCEUSTATE_RLSB, "LPPU" }, { 0 } }; diff --git a/src/boards/88.c b/src/boards/88.c index 7341f25..dd0932d 100644 --- a/src/boards/88.c +++ b/src/boards/88.c @@ -64,6 +64,7 @@ static void M88Power(void) { } static void StateRestore(int version) { + cmd &= 7; /* reg[] has 8 entries; clamp savestate value */ Sync(); MSync(); } diff --git a/src/boards/KS7017.c b/src/boards/KS7017.c index 1e4a830..8a5549a 100644 --- a/src/boards/KS7017.c +++ b/src/boards/KS7017.c @@ -33,9 +33,9 @@ static SFORMAT StateRegs[] = { { &mirr, 1, "MIRR" }, { ®, 1, "REGS" }, - { &IRQa, 4, "IRQA" }, - { &IRQCount, 4, "IRQC" }, - { &IRQLatch, 4, "IRQL" }, + { &IRQa, 4 | FCEUSTATE_RLSB, "IRQA" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQLatch, 4 | FCEUSTATE_RLSB, "IRQL" }, { &latche, 1, "LATC" }, { 0 } }; diff --git a/src/boards/KS7037.c b/src/boards/KS7037.c index 0c73c36..0835f64 100644 --- a/src/boards/KS7037.c +++ b/src/boards/KS7037.c @@ -94,6 +94,7 @@ static void Close(void) { } static void StateRestore(int version) { + cmd &= 7; /* reg[] has 8 entries; clamp savestate value */ WSync(); } diff --git a/src/boards/__dummy_mapper.c b/src/boards/__dummy_mapper.c index 0e257eb..91275ac 100644 --- a/src/boards/__dummy_mapper.c +++ b/src/boards/__dummy_mapper.c @@ -34,8 +34,8 @@ static SFORMAT StateRegs[] = { { reg, 8, "REGS" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 2, "IRQC" }, - { &IRQLatch, 2, "IRQL" }, + { &IRQCount, 2 | FCEUSTATE_RLSB, "IRQC" }, + { &IRQLatch, 2 | FCEUSTATE_RLSB, "IRQL" }, { 0 } }; diff --git a/src/boards/addrlatch.c b/src/boards/addrlatch.c index fe9b9fd..770bf9e 100644 --- a/src/boards/addrlatch.c +++ b/src/boards/addrlatch.c @@ -87,7 +87,7 @@ static void Latch_Init(CartInfo *info, void (*proc)(void), readfunc func, uint16 AddExState(WRAM, WRAMSIZE, 0, "WRAM"); } GameStateRestore = StateRestore; - AddExState(&latche, 2, 0, "LATC"); + AddExState(&latche, 2, 1, "LATC"); } /*------------------ BMCD1038 ---------------------------*/ diff --git a/src/boards/asic_latch.c b/src/boards/asic_latch.c index 579de14..c40d263 100644 --- a/src/boards/asic_latch.c +++ b/src/boards/asic_latch.c @@ -28,7 +28,7 @@ uint16 Latch_address; uint8 Latch_data; static SFORMAT Latch_state[] = { - {&Latch_address, 2, "LATA" }, + {&Latch_address, 2 | FCEUSTATE_RLSB, "LATA" }, {&Latch_data, 1, "LATD" }, { 0 } }; diff --git a/src/boards/asic_mmc3.c b/src/boards/asic_mmc3.c index 3c78b02..4293d60 100644 --- a/src/boards/asic_mmc3.c +++ b/src/boards/asic_mmc3.c @@ -107,7 +107,10 @@ void MMC3_syncMirror () { void MMC3_clockCounter () { uint8 prevCounter = MMC3_counter; - MMC3_counter = MMC3_reloadRequest || !MMC3_counter? MMC3_reloadValue: --MMC3_counter; + if (MMC3_reloadRequest || !MMC3_counter) + MMC3_counter = MMC3_reloadValue; + else + MMC3_counter--; if ((prevCounter || MMC3_type != MMC3_TYPE_NEC || MMC3_reloadRequest) && !MMC3_counter && MMC3_irqEnable) X6502_IRQBegin(FCEU_IQEXT); MMC3_reloadRequest = 0; } diff --git a/src/boards/asic_vrc3.c b/src/boards/asic_vrc3.c index 2f46e4e..9f1fa0b 100644 --- a/src/boards/asic_vrc3.c +++ b/src/boards/asic_vrc3.c @@ -30,8 +30,8 @@ static uint16 VRC3_reload; static SFORMAT VRC3_state[] = { {&VRC3_prg, 1, "VC3P" }, {&VRC3_irq, 1, "VC3I" }, - {&VRC3_count, 1, "VC3C" }, - {&VRC3_reload, 1, "VC3R" }, + {&VRC3_count, 2 | FCEUSTATE_RLSB, "VC3C" }, + {&VRC3_reload, 2 | FCEUSTATE_RLSB, "VC3R" }, { 0 } }; diff --git a/src/boards/bj56.c b/src/boards/bj56.c index a73a2ce..852b341 100644 --- a/src/boards/bj56.c +++ b/src/boards/bj56.c @@ -35,7 +35,7 @@ static SFORMAT StateRegs[] = { { preg, 4, "PREG" }, { creg, 8, "CREG" }, - { &IRQCount, 4, "IRQC" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, { 0 } }; diff --git a/src/boards/coolgirl.c b/src/boards/coolgirl.c index 92a6274..2d6ba3f 100644 --- a/src/boards/coolgirl.c +++ b/src/boards/coolgirl.c @@ -2255,6 +2255,11 @@ static void COOLGIRL_Restore(int version) { } #define ExState(var, varname) AddExState(&var, sizeof(var), 0, varname) +/* Variant for multi-byte single-variable state. Asks the savestate + * machinery to byte-swap the value on BE hosts so cross-platform + * savestates round-trip correctly. Must NOT be used for arrays + * of multi-byte values (the swap is whole-buffer, not per-element). */ +#define ExStateLE(var, varname) AddExState(&var, sizeof(var), 1, varname) void COOLGIRL_Init(CartInfo *info) { size_t i; @@ -2296,27 +2301,27 @@ void COOLGIRL_Init(CartInfo *info) { ExState(mirroring, "MIRR"); ExState(four_screen, "4SCR"); ExState(lockout, "LOCK"); - ExState(prg_base, "PBAS"); - ExState(prg_mask, "PMSK"); + ExStateLE(prg_base, "PBAS"); + ExStateLE(prg_mask, "PMSK"); ExState(prg_mode, "PMOD"); ExState(prg_bank_6000, "P6BN"); ExState(prg_bank_a, "PABN"); ExState(prg_bank_b, "PBBN"); ExState(prg_bank_c, "PCBN"); ExState(prg_bank_d, "PDBN"); - ExState(chr_mask, "CMSK"); + ExStateLE(chr_mask, "CMSK"); ExState(chr_mode, "CMOD"); - ExState(chr_bank_a, "CABN"); - ExState(chr_bank_b, "CBBN"); - ExState(chr_bank_c, "CCBN"); - ExState(chr_bank_d, "CDBN"); - ExState(chr_bank_e, "CEBN"); - ExState(chr_bank_f, "CFBN"); - ExState(chr_bank_g, "CGBN"); - ExState(chr_bank_h, "CHBN"); + ExStateLE(chr_bank_a, "CABN"); + ExStateLE(chr_bank_b, "CBBN"); + ExStateLE(chr_bank_c, "CCBN"); + ExStateLE(chr_bank_d, "CDBN"); + ExStateLE(chr_bank_e, "CEBN"); + ExStateLE(chr_bank_f, "CFBN"); + ExStateLE(chr_bank_g, "CGBN"); + ExStateLE(chr_bank_h, "CHBN"); ExState(ppu_latch0, "PPU0"); ExState(ppu_latch1, "PPU1"); - ExState(lreset, "LRST"); + ExStateLE(lreset, "LRST"); ExState(mmc1_load_register, "M01R"); ExState(mmc3_internal, "M01I"); ExState(mapper69_internal, "M69I"); @@ -2337,32 +2342,32 @@ void COOLGIRL_Init(CartInfo *info) { ExState(mmc5_irq_enabled, "M5IE"); ExState(mmc5_irq_line, "M5IL"); ExState(mmc5_irq_out, "M5IO"); - ExState(mapper18_irq_value, "18IV"); + ExStateLE(mapper18_irq_value, "18IV"); ExState(mapper18_irq_control, "18IC"); - ExState(mapper18_irq_latch, "18IL"); + ExStateLE(mapper18_irq_latch, "18IL"); ExState(mapper65_irq_enabled, "65IE"); - ExState(mapper65_irq_value, "65IV"); - ExState(mapper65_irq_latch, "65IL"); + ExStateLE(mapper65_irq_value, "65IV"); + ExStateLE(mapper65_irq_latch, "65IL"); ExState(mapper69_irq_enabled, "69IE"); ExState(mapper69_counter_enabled, "69CE"); - ExState(mapper69_irq_value, "69IV"); + ExStateLE(mapper69_irq_value, "69IV"); ExState(vrc4_irq_value, "V4IV"); ExState(vrc4_irq_control, "V4IC"); ExState(vrc4_irq_latch, "V4IL"); ExState(vrc4_irq_prescaler, "V4PP"); ExState(vrc4_irq_prescaler_counter, "V4PC"); - ExState(vrc3_irq_value, "V3IV"); + ExStateLE(vrc3_irq_value, "V3IV"); ExState(vrc3_irq_control, "V3IC"); - ExState(vrc3_irq_latch, "V3IL"); + ExStateLE(vrc3_irq_latch, "V3IL"); ExState(mapper42_irq_enabled, "42IE"); - ExState(mapper42_irq_value, "42IV"); + ExStateLE(mapper42_irq_value, "42IV"); ExState(mapper83_irq_enabled_latch, "M83L"); ExState(mapper83_irq_enabled, "M83I"); - ExState(mapper83_irq_counter, "M83C"); + ExStateLE(mapper83_irq_counter, "M83C"); ExState(mapper90_xor, "90XR"); ExState(mapper67_irq_enabled, "67IE"); ExState(mapper67_irq_latch, "67IL"); - ExState(mapper67_irq_counter, "67IC"); + ExStateLE(mapper67_irq_counter, "67IC"); ExState(flash_state, "FLST"); ExState(flash_buffer_a, "FLBA"); ExState(flash_buffer_v, "FLBV"); diff --git a/src/boards/jyasic.c b/src/boards/jyasic.c index ef0d708..5ef5f1c 100644 --- a/src/boards/jyasic.c +++ b/src/boards/jyasic.c @@ -194,16 +194,20 @@ static void clockIRQ (void) if (irqEnabled) switch (irqControl &0xC0) { - case 0x40: - irqPrescaler =(irqPrescaler &~mask) | (++irqPrescaler &mask); - if ((irqPrescaler &mask) ==0x00 && (irqControl &0x08? irqCounter: ++irqCounter) ==0x00) + case 0x40: { + uint8_t newp = (irqPrescaler + 1) & mask; + irqPrescaler = (irqPrescaler & ~mask) | newp; + if (newp == 0x00 && (irqControl &0x08? irqCounter: ++irqCounter) ==0x00) X6502_IRQBegin(FCEU_IQEXT); break; - case 0x80: - irqPrescaler =(irqPrescaler &~mask) | (--irqPrescaler &mask); - if ((irqPrescaler &mask) ==mask && (irqControl &0x08? irqCounter: --irqCounter) ==0xFF) + } + case 0x80: { + uint8_t newp = (irqPrescaler - 1) & mask; + irqPrescaler = (irqPrescaler & ~mask) | newp; + if (newp == mask && (irqControl &0x08? irqCounter: --irqCounter) ==0xFF) X6502_IRQBegin(FCEU_IQEXT); break; + } } } diff --git a/src/boards/lh53.c b/src/boards/lh53.c index 1ee1592..6dc42bb 100644 --- a/src/boards/lh53.c +++ b/src/boards/lh53.c @@ -33,7 +33,7 @@ static SFORMAT StateRegs[] = { { ®, 1, "REG" }, { &IRQa, 1, "IRQA" }, - { &IRQCount, 4, "IRQC" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, { 0 } }; diff --git a/src/boards/mihunche.c b/src/boards/mihunche.c index e41ced4..9f378c9 100644 --- a/src/boards/mihunche.c +++ b/src/boards/mihunche.c @@ -24,7 +24,7 @@ static uint16 latche; static SFORMAT StateRegs[] = { - { &latche, 2, "LATC" }, + { &latche, 2 | FCEUSTATE_RLSB, "LATC" }, { 0 } }; diff --git a/src/boards/mmc2and4.c b/src/boards/mmc2and4.c index e5c8f40..d3e7b88 100644 --- a/src/boards/mmc2and4.c +++ b/src/boards/mmc2and4.c @@ -103,6 +103,11 @@ static void MMC2and4Power(void) { } static void StateRestore(int version) { + /* latch0/latch1 are written as boolean (0 or 1) and used as + * indices into creg[4] via creg[latch0] and creg[latch1 + 2]. + * Clamp savestate-loaded values to keep the indices in range. */ + latch0 &= 1; + latch1 &= 1; Sync(); } diff --git a/src/boards/mmc3.c b/src/boards/mmc3.c index 4b2aa4a..baeded3 100644 --- a/src/boards/mmc3.c +++ b/src/boards/mmc3.c @@ -1520,8 +1520,8 @@ static uint32 m555_count_target = 0x20000000; static SFORMAT M555StateRegs[] = { { m555_reg, 2, "REGS" }, - { &m555_count, 2, "CNTR" }, - { &m555_count_expired, 2, "CNTE" }, + { &m555_count, 4 | FCEUSTATE_RLSB, "CNTR" }, + { &m555_count_expired, 1, "CNTE" }, { 0 } }; diff --git a/src/boards/mmc5.c b/src/boards/mmc5.c index 77fe740..5a0f788 100644 --- a/src/boards/mmc5.c +++ b/src/boards/mmc5.c @@ -670,6 +670,20 @@ void NSFMMC5_Close(void) { ExRAM = NULL; } +/* Cart-side close. GenMMC5_Init allocates WRAM (optional), MMC5fill, and + * ExRAM. Without this, every MMC5 cart load/unload cycle leaks 2-66 KB. */ +static void GenMMC5_Close(void) { + if (WRAM) + FCEU_gfree(WRAM); + WRAM = NULL; + if (MMC5fill) + FCEU_gfree(MMC5fill); + MMC5fill = NULL; + if (ExRAM) + FCEU_gfree(ExRAM); + ExRAM = NULL; +} + static void GenMMC5Reset(void) { int x; @@ -798,6 +812,7 @@ static void GenMMC5_Init(CartInfo *info, int wsize, int battery) { BuildWRAMSizeTable(); GameStateRestore = MMC5_StateRestore; info->Power = GenMMC5Reset; + info->Close = GenMMC5_Close; if (battery) { info->SaveGame[0] = WRAM; diff --git a/src/boards/onebus.c b/src/boards/onebus.c index 534a749..673c61d 100644 --- a/src/boards/onebus.c +++ b/src/boards/onebus.c @@ -61,10 +61,10 @@ static SFORMAT StateRegs[] = { &IRQa, 1, "IRQA" }, { &pcm_enable, 1, "PCME" }, { &pcm_irq, 1, "PCMI" }, - { &pcm_addr, 2, "PCMA" }, - { &pcm_size, 2, "PCMS" }, - { &pcm_latch, 2, "PCML" }, - { &pcm_clock, 2, "PCMC" }, + { &pcm_addr, 2 | FCEUSTATE_RLSB, "PCMA" }, + { &pcm_size, 2 | FCEUSTATE_RLSB, "PCMS" }, + { &pcm_latch, 2 | FCEUSTATE_RLSB, "PCML" }, + { &pcm_clock, 2 | FCEUSTATE_RLSB, "PCMC" }, { ®4242, 1, "4242" }, { &dipswitch, 1, "DIPS" }, { 0 } @@ -375,6 +375,12 @@ void UNLOneBus_Close(void) { if (WRAM) FCEU_gfree(WRAM); WRAM = NULL; + /* Mapper270_Init / Mapper436_Init allocate an additional CHRRAM after + * UNLOneBus_Init returns; free that here too to avoid leaking it on + * every load/unload cycle. */ + if (CHRRAM) + FCEU_gfree(CHRRAM); + CHRRAM = NULL; } void UNLOneBus_Init(CartInfo *info) { @@ -385,8 +391,16 @@ void UNLOneBus_Init(CartInfo *info) { if (info->iNES2) submapper =info->submapper; - else - submapper =(((*(uint32*)&(info->MD5)) == 0x305fcdc3) || ((*(uint32*)&(info->MD5)) == 0x6abfce8e))? 2: 0; /* PowerJoy Supermax Carts */ + else { + /* Compare the first four MD5 bytes literally to avoid the previous + * endian-dependent (uint32) reinterpretation, which silently + * mismatched on big-endian hosts and disabled the PowerJoy Supermax + * variant detection. */ + const uint8 *md5 = info->MD5; + int is_305fcdc3 = (md5[0] == 0xc3 && md5[1] == 0xcd && md5[2] == 0x5f && md5[3] == 0x30); + int is_6abfce8e = (md5[0] == 0x8e && md5[1] == 0xce && md5[2] == 0xbf && md5[3] == 0x6a); + submapper = (is_305fcdc3 || is_6abfce8e) ? 2 : 0; /* PowerJoy Supermax Carts */ + } cpuMangle =cpuMangles[submapper]; ppuMangle =ppuMangles[submapper]; diff --git a/src/boards/pec-586.c b/src/boards/pec-586.c index 536905d..007c521 100644 --- a/src/boards/pec-586.c +++ b/src/boards/pec-586.c @@ -28,7 +28,7 @@ static uint32 WRAMSIZE; static SFORMAT StateRegs[] = { { reg, 2, "REG" }, - { &lastnt, 4, "LNT" }, + { &lastnt, 4 | FCEUSTATE_RLSB, "LNT" }, { 0 } }; diff --git a/src/boards/pic16c5x.c b/src/boards/pic16c5x.c index 1443e00..775c530 100644 --- a/src/boards/pic16c5x.c +++ b/src/boards/pic16c5x.c @@ -977,25 +977,25 @@ void pic16c5x_run(void) void AddExState(void *v, uint32 s, int type, char *desc); void pic16c5x_add_statesinfo(void) { - AddExState(&m_PC, sizeof(m_PC), 0, "PC00"); - AddExState(&m_PREVPC, sizeof(m_PREVPC), 0, "PRPC"); + AddExState(&m_PC, sizeof(m_PC), 1, "PC00"); + AddExState(&m_PREVPC, sizeof(m_PREVPC), 1, "PRPC"); AddExState(&m_W, sizeof(m_W), 0, "W000"); AddExState(&m_OPTION, sizeof(m_OPTION), 0, "OPT0"); - AddExState(&m_CONFIG, sizeof(m_CONFIG), 0, "CONF"); + AddExState(&m_CONFIG, sizeof(m_CONFIG), 1, "CONF"); AddExState(&m_ALU, sizeof(m_ALU), 0, "ALU"); - AddExState(&m_WDT, sizeof(m_WDT), 0, "WDT"); + AddExState(&m_WDT, sizeof(m_WDT), 1, "WDT"); AddExState(&m_TRISA, sizeof(m_TRISA), 0, "TRSA"); AddExState(&m_TRISB, sizeof(m_TRISB), 0, "TRSB"); AddExState(&m_TRISC, sizeof(m_TRISC), 0, "TRSC"); - AddExState(&m_STACK[0], sizeof(m_STACK[0]), 0, "STC0"); - AddExState(&m_STACK[1], sizeof(m_STACK[1]), 0, "STC1"); - AddExState(&m_prescaler, sizeof(m_prescaler), 0, "PSCL"); - AddExState(&m_opcode, sizeof(m_opcode), 0, "OPS0"); + AddExState(&m_STACK[0], sizeof(m_STACK[0]), 1, "STC0"); + AddExState(&m_STACK[1], sizeof(m_STACK[1]), 1, "STC1"); + AddExState(&m_prescaler, sizeof(m_prescaler), 1, "PSCL"); + AddExState(&m_opcode, sizeof(m_opcode), 1, "OPS0"); AddExState(m_internalram, sizeof(m_internalram), 0, "IRAM"); - AddExState(&m_icount, sizeof(m_icount), 0, "ICNT"); - AddExState(&m_delay_timer, sizeof(m_delay_timer), 0, "DTIM"); - AddExState(&m_rtcc, sizeof(m_rtcc), 0, "RTCC"); + AddExState(&m_icount, sizeof(m_icount), 1, "ICNT"); + AddExState(&m_delay_timer, sizeof(m_delay_timer), 1, "DTIM"); + AddExState(&m_rtcc, sizeof(m_rtcc), 1, "RTCC"); AddExState(&m_count_pending, sizeof(m_count_pending), 0, "CNTP"); - AddExState(&m_inst_cycles, sizeof(m_inst_cycles), 0, "ICYC"); - AddExState(&m_clock2cycle, sizeof(m_clock2cycle), 0, "CL2C"); + AddExState(&m_inst_cycles, sizeof(m_inst_cycles), 1, "ICYC"); + AddExState(&m_clock2cycle, sizeof(m_clock2cycle), 1, "CL2C"); } \ No newline at end of file diff --git a/src/boards/sachen.c b/src/boards/sachen.c index 4149695..7ee97a3 100644 --- a/src/boards/sachen.c +++ b/src/boards/sachen.c @@ -90,6 +90,7 @@ static void S8259Reset(void) { } static void S8259Restore(int version) { + cmd &= 7; /* latch[] has 8 entries; clamp savestate value */ S8259Synco(); } @@ -298,6 +299,7 @@ static DECLFW(S74LS374NWrite) { } static void S74LS374NRestore(int version) { + cmd &= 7; /* latch[] has 8 entries; clamp savestate value */ S74LS374NSynco(); } diff --git a/src/boards/unrom512.c b/src/boards/unrom512.c index 4f136d0..1da73f1 100644 --- a/src/boards/unrom512.c +++ b/src/boards/unrom512.c @@ -83,17 +83,51 @@ void setfprg16(uint32 A, uint32 V) { } } +/* The flash write count region (the first flash_write_count_size bytes of + * fceumm_flash_buf) is exposed verbatim to the frontend as battery save + * RAM. To keep .srm files portable between LE and BE builds, we always + * store the counters as little-endian on disk; on BE hosts that means + * byte-swapping at every counter access. */ +static INLINE uint32 fwc_load(uint32 idx) { +#ifdef MSB_FIRST + uint8 *p = (uint8 *)&flash_write_count[idx]; + return (uint32)p[0] | ((uint32)p[1] << 8) | ((uint32)p[2] << 16) | ((uint32)p[3] << 24); +#else + return flash_write_count[idx]; +#endif +} + +static INLINE void fwc_store(uint32 idx, uint32 v) { +#ifdef MSB_FIRST + uint8 *p = (uint8 *)&flash_write_count[idx]; + p[0] = (uint8)v; + p[1] = (uint8)(v >> 8); + p[2] = (uint8)(v >> 16); + p[3] = (uint8)(v >> 24); +#else + flash_write_count[idx] = v; +#endif +} + void inc_flash_write_count(uint8 bank, uint32 A) { - flash_write_count[(bank * 4) + ((A & 0x3000) >> 12)]++; - if (!flash_write_count[(bank * 4) + ((A & 0x3000) >> 12)]) - flash_write_count[(bank * 4) + ((A & 0x3000) >> 12)]++; + uint32 idx = (bank * 4) + ((A & 0x3000) >> 12); + uint32 v = fwc_load(idx) + 1; + if (v == 0) v = 1; /* avoid wrap to 0 (which means "never written") */ + fwc_store(idx, v); } uint32 GetFlashWriteCount(uint8 bank, uint32 A) { - return flash_write_count[(bank * 4) + ((A & 0x3000) >> 12)]; + return fwc_load((bank * 4) + ((A & 0x3000) >> 12)); } static void StateRestore(int version) { + /* erase_a/d/b[] are 5-element arrays indexed by flash_state. + * Legitimate values are 0..4; clamp savestate values that exceed + * the array to a safe value to avoid OOB reads. */ + if (flash_state >= 5) + flash_state = 0; + if (flash_mode > 2) + flash_mode = 0; WHSync(); } @@ -261,7 +295,7 @@ void UNROM512_Init(CartInfo *info) { AddExState(&flash_state, 1, 0, "FLASH_STATE"); AddExState(&flash_mode, 1, 0, "FLASH_MODE"); AddExState(&flash_bank, 1, 0, "FLASH_BANK"); - AddExState(&latcha, 2, 0, "LATA"); + AddExState(&latcha, 2, 1, "LATA"); } AddExState(&latche, 1, 0, "LATC"); AddExState(&bus_conflict, 1, 0, "BUSC"); diff --git a/src/boards/vrc3.c b/src/boards/vrc3.c index 6e32d49..59be687 100644 --- a/src/boards/vrc3.c +++ b/src/boards/vrc3.c @@ -37,8 +37,8 @@ static SFORMAT StateRegs[] = { &IRQa, 1, "IRQA" }, { &IRQx, 1, "IRQX" }, { &IRQm, 1, "IRQM" }, - { &IRQReload, 2, "IRQR" }, - { &IRQCount, 2, "IRQC" }, + { &IRQReload, 2 | FCEUSTATE_RLSB, "IRQR" }, + { &IRQCount, 2 | FCEUSTATE_RLSB, "IRQC" }, { 0 } }; diff --git a/src/boards/vrc6.c b/src/boards/vrc6.c index 0574cef..b58881f 100644 --- a/src/boards/vrc6.c +++ b/src/boards/vrc6.c @@ -38,8 +38,8 @@ static SFORMAT StateRegs[] = { &IRQa, 1, "IRQA" }, { &IRQd, 1, "IRQD" }, { &IRQLatch, 1, "IRQL" }, - { &IRQCount, 4, "IRQC" }, - { &CycleCount, 4, "CYCC" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &CycleCount, 4 | FCEUSTATE_RLSB, "CYCC" }, { 0 } }; diff --git a/src/boards/vrc7.c b/src/boards/vrc7.c index 2d77fa2..694d159 100644 --- a/src/boards/vrc7.c +++ b/src/boards/vrc7.c @@ -38,8 +38,8 @@ static SFORMAT StateRegs[] = { &IRQa, 1, "IRQA" }, { &IRQd, 1, "IRQD" }, { &IRQLatch, 1, "IRQL" }, - { &IRQCount, 4, "IRQC" }, - { &CycleCount, 4, "CYCC" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &CycleCount, 4 | FCEUSTATE_RLSB, "CYCC" }, { 0 } }; diff --git a/src/boards/vrc7p.c b/src/boards/vrc7p.c index f1d28ca..4e70a05 100644 --- a/src/boards/vrc7p.c +++ b/src/boards/vrc7p.c @@ -34,8 +34,8 @@ static SFORMAT StateRegs[] = { &IRQa, 1, "IRQA" }, { &IRQd, 1, "IRQD" }, { &IRQLatch, 1, "IRQL" }, - { &IRQCount, 4, "IRQC" }, - { &CycleCount, 4, "CYCC" }, + { &IRQCount, 4 | FCEUSTATE_RLSB, "IRQC" }, + { &CycleCount, 4 | FCEUSTATE_RLSB, "CYCC" }, { 0 } }; diff --git a/src/boards/vrcirq.c b/src/boards/vrcirq.c index d12d3d3..4639170 100644 --- a/src/boards/vrcirq.c +++ b/src/boards/vrcirq.c @@ -10,7 +10,7 @@ static uint8 IRQm; static SFORMAT StateRegs[] = { - { &IRQPrescaler, 2, "PREC" }, + { &IRQPrescaler, 2 | FCEUSTATE_RLSB, "PREC" }, { &IRQCount, 1, "IRQC" }, { &IRQLatch, 1, "IRQL" }, { &IRQd, 1, "IRQD" }, diff --git a/src/cart.c b/src/cart.c index 9d4a5d6..05b595c 100644 --- a/src/cart.c +++ b/src/cart.c @@ -151,8 +151,18 @@ DECLFR(CartBROB) { } void FASTAPASS(3) setprg2r(int r, uint32 A, uint32 V) { + /* 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; + } V &= PRGmask2[r]; - setpageptr(2, A, PRGptr[r] ? (&PRGptr[r][V << 11]) : 0, PRGram[r]); + setpageptr(2, A, &PRGptr[r][V << 11], PRGram[r]); } void FASTAPASS(2) setprg2(uint32 A, uint32 V) { @@ -160,8 +170,23 @@ void FASTAPASS(2) setprg2(uint32 A, uint32 V) { } void FASTAPASS(3) setprg4r(int r, uint32 A, uint32 V) { + 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; + } V &= PRGmask4[r]; - setpageptr(4, A, PRGptr[r] ? (&PRGptr[r][V << 12]) : 0, PRGram[r]); + setpageptr(4, A, &PRGptr[r][V << 12], PRGram[r]); } void FASTAPASS(2) setprg4(uint32 A, uint32 V) { @@ -220,6 +245,7 @@ void FASTAPASS(2) setprg32(uint32 A, uint32 V) { void FASTAPASS(3) setchr1r(int r, uint32 A, uint32 V) { if (!CHRptr[r]) return; + if (CHRsize[r] < 1024) return; /* mask underflow guard */ FCEUPPU_LineUpdate(); V &= CHRmask1[r]; if (CHRram[r]) @@ -231,6 +257,7 @@ void FASTAPASS(3) setchr1r(int r, uint32 A, uint32 V) { void FASTAPASS(3) setchr2r(int r, uint32 A, uint32 V) { if (!CHRptr[r]) return; + if (CHRsize[r] < 2048) return; /* mask underflow guard */ FCEUPPU_LineUpdate(); V &= CHRmask2[r]; VPageR[(A) >> 10] = VPageR[((A) >> 10) + 1] = &CHRptr[r][(V) << 11] - (A); @@ -242,6 +269,7 @@ void FASTAPASS(3) setchr2r(int r, uint32 A, uint32 V) { void FASTAPASS(3) setchr4r(int r, uint32 A, uint32 V) { if (!CHRptr[r]) return; + if (CHRsize[r] < 4096) return; /* mask underflow guard */ FCEUPPU_LineUpdate(); V &= CHRmask4[r]; VPageR[(A) >> 10] = VPageR[((A) >> 10) + 1] = @@ -257,7 +285,15 @@ void FASTAPASS(2) setchr8r(int r, uint32 V) { if (!CHRptr[r]) return; FCEUPPU_LineUpdate(); - V &= CHRmask8[r]; + 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]; + } for (x = 7; x >= 0; x--) VPageR[x] = &CHRptr[r][V << 13]; if (CHRram[r]) diff --git a/src/cheat.c b/src/cheat.c index e8243c0..2161416 100644 --- a/src/cheat.c +++ b/src/cheat.c @@ -114,13 +114,21 @@ void RebuildSubCheats(void) { if (c->type == 1 && c->status) { if (GetReadHandler(c->addr) == SubCheatsRead) { /* Prevent a catastrophe by this check. */ - } else { + } else if (numsubcheats < (int)(sizeof(SubCheats) / sizeof(SubCheats[0]))) { SubCheats[numsubcheats].PrevRead = GetReadHandler(c->addr); SubCheats[numsubcheats].addr = c->addr; SubCheats[numsubcheats].val = c->val; SubCheats[numsubcheats].compare = c->compare; SetReadHandler(c->addr, c->addr, SubCheatsRead); numsubcheats++; + } else { + /* SubCheats[] table is full; further substitution + * cheats are dropped silently rather than overflowing. */ + static int warned = 0; + if (!warned) { + FCEU_PrintError(" Too many active substitution cheats; some have been ignored.\n"); + warned = 1; + } } } c = c->next; diff --git a/src/drivers/libretro/libretro.c b/src/drivers/libretro/libretro.c index 4eda5b0..81b5902 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -1050,6 +1050,13 @@ static void stereo_filter_apply_delay(int32_t *sound_buffer, size_t size) tmp_buffer_size = (tmp_buffer_size << 1) - (tmp_buffer_size >> 1); tmp_buffer = (int32_t *)malloc(tmp_buffer_size * sizeof(int32_t)); + if (!tmp_buffer) + { + /* On allocation failure, drop this batch rather than dereferencing + * a NULL buffer in the memcpy below. The next batch will retry. */ + return; + } + memcpy(tmp_buffer, stereo_filter_delay.samples, stereo_filter_delay.samples_pos * sizeof(int32_t)); @@ -1137,6 +1144,15 @@ static void stereo_filter_init_delay(void) stereo_filter_delay.samples = (int32_t *)malloc( initial_samples_size * sizeof(int32_t)); + if (!stereo_filter_delay.samples) + { + /* Fall back to the null filter on allocation failure rather than + * leaving a NULL samples buffer that the apply path would deref. */ + stereo_filter_delay.samples_size = 0; + stereo_filter_delay.samples_pos = 0; + stereo_filter_apply = stereo_filter_apply_null; + return; + } stereo_filter_delay.samples_size = initial_samples_size; stereo_filter_delay.samples_pos = 0; @@ -1433,7 +1449,7 @@ void retro_set_controller_port_device(unsigned port, unsigned device) { if (device != RETRO_DEVICE_AUTO) update_nes_controllers(port, device); - else + else if (GameInfo) /* GameInfo may be NULL pre-load */ update_nes_controllers(port, nes_to_libretro(GameInfo->input[port])); } else @@ -1460,7 +1476,7 @@ void retro_set_controller_port_device(unsigned port, unsigned device) { if (device != RETRO_DEVICE_FC_AUTO) update_nes_controllers(4, device); - else + else if (GameInfo) /* GameInfo may be NULL pre-load */ update_nes_controllers(4, fc_to_libretro(GameInfo->inputfc)); } @@ -1921,7 +1937,9 @@ void retro_deinit (void) FCEUI_Sound(0); FCEUI_Kill(); #if defined(_3DS) - linearFree(fceu_video_out); + if (fceu_video_out) + linearFree(fceu_video_out); + fceu_video_out = NULL; #else if (fceu_video_out) free(fceu_video_out); @@ -3057,6 +3075,8 @@ size_t retro_serialize_size(void) { /* Something arbitrarily big.*/ uint8_t *buffer = (uint8_t*)malloc(1000000); + if (!buffer) + return 0; memstream_set_buffer(buffer, 1000000); FCEUSS_Save_Mem(); @@ -3130,7 +3150,7 @@ void retro_cheat_reset(void) void retro_cheat_set(unsigned index, bool enabled, const char *code) { char name[256]; - char temp[256]; + char temp[1024]; char *codepart; uint16 a; uint8 v; @@ -3140,8 +3160,12 @@ void retro_cheat_set(unsigned index, bool enabled, const char *code) if (code == NULL) return; + /* Cheat strings can be arbitrarily long (multiple codes joined by + * separators). The previous strcpy into a 256-byte stack buffer was + * trivially overflowable. Use strlcpy and a larger buffer; truncate + * if necessary rather than overflow. */ sprintf(name, "N/A"); - strcpy(temp, code); + strlcpy(temp, code, sizeof(temp)); codepart = strtok(temp, "+,;._ "); while (codepart) @@ -3685,6 +3709,17 @@ bool retro_load_game(const struct retro_game_info *info) if (!GameInfo) { + /* On load failure the libretro frontend won't call retro_unload_game, + * so free the framebuffer we just allocated to avoid leaking ~256 KB + * per failed load. */ +#if defined(_3DS) + if (fceu_video_out) + linearFree(fceu_video_out); +#elif !defined(PSP) + if (fceu_video_out) + free(fceu_video_out); +#endif + fceu_video_out = NULL; #if 0 /* An error message here is superfluous - the frontend * will report that content loading has failed */ @@ -3858,7 +3893,7 @@ void *retro_get_memory_data(unsigned type) return iNESCart.SaveGame[0]; else if (UNIFCart.battery && UNIFCart.SaveGame[0] && UNIFCart.SaveGameLen[0]) return UNIFCart.SaveGame[0]; - else if (GameInfo->type == GIT_FDS) + else if (GameInfo && GameInfo->type == GIT_FDS) return FDSROM_ptr(); else data = NULL; @@ -3885,7 +3920,7 @@ size_t retro_get_memory_size(unsigned type) size = iNESCart.SaveGameLen[0]; else if (UNIFCart.battery && UNIFCart.SaveGame[0] && UNIFCart.SaveGameLen[0]) size = UNIFCart.SaveGameLen[0]; - else if (GameInfo->type == GIT_FDS) + else if (GameInfo && GameInfo->type == GIT_FDS) size = FDSROM_size(); else size = 0; diff --git a/src/fceu-endian.c b/src/fceu-endian.c index 32aec45..8a322dc 100644 --- a/src/fceu-endian.c +++ b/src/fceu-endian.c @@ -28,12 +28,17 @@ void FlipByteOrder(uint8 *src, uint32 count) { uint8 *start = src; - uint8 *end = src + count - 1; + uint8 *end; if ((count & 1) || !count) return; /* This shouldn't happen. */ - while (count--) + end = src + count - 1; + /* Iterate while start < end, not 'count' times: the original loop + * over-iterated and effectively performed a no-op on every even count. + * That broke savestate portability across LE<->BE for every + * FCEUSTATE_RLSB-marked field. */ + while (start < end) { uint8 tmp; diff --git a/src/fceu-memory.c b/src/fceu-memory.c index 2b99dfb..69c64b7 100644 --- a/src/fceu-memory.c +++ b/src/fceu-memory.c @@ -45,7 +45,7 @@ void *FCEU_malloc(uint32 size) if (!ret) { FCEU_PrintError("Error allocating memory!"); - ret = 0; + return NULL; } memset(ret, 0, size); return ret; diff --git a/src/fceu.c b/src/fceu.c index 532c1a0..6214fc1 100644 --- a/src/fceu.c +++ b/src/fceu.c @@ -82,7 +82,15 @@ int AllocGenieRW(void) if (!BWriteG) { if (!(BWriteG = (writefunc*)FCEU_malloc(0x8000 * sizeof(writefunc)))) + { + /* Free AReadG too on partial-failure to avoid leaking a + * 256 KB read-handler table. Subsequent calls would otherwise + * see AReadG != NULL and skip its alloc, but RWWrap stays 0 + * because we return 0, so the table is never used or freed. */ + free(AReadG); + AReadG = NULL; return 0; + } } else memset(BWriteG, 0, 0x8000 * sizeof(writefunc)); @@ -240,6 +248,8 @@ FCEUGI *FCEUI_LoadGame(const char *name, const uint8_t *databuf, size_t databufs ResetGameLoaded(); GameInfo = malloc(sizeof(FCEUGI)); + if (!GameInfo) + return NULL; memset(GameInfo, 0, sizeof(FCEUGI)); GameInfo->soundchan = 0; @@ -461,7 +471,10 @@ void FCEU_printf(char *format, ...) va_list ap; va_start(ap, format); - vsprintf(temp, format, ap); + /* vsnprintf instead of vsprintf so a long expansion (e.g. of a ROM + * filename or an attacker-controlled string field) is truncated + * rather than overflowing the stack buffer. */ + vsnprintf(temp, sizeof(temp), format, ap); FCEUD_Message(temp); va_end(ap); @@ -474,7 +487,7 @@ void FCEU_PrintError(char *format, ...) va_list ap; va_start(ap, format); - vsprintf(temp, format, ap); + vsnprintf(temp, sizeof(temp), format, ap); FCEUD_PrintError(temp); va_end(ap); diff --git a/src/fds.c b/src/fds.c index 196fbd0..eb9124a 100644 --- a/src/fds.c +++ b/src/fds.c @@ -120,6 +120,38 @@ void FDSGI(int h) { static void FDSStateRestore(int version) { int x; + /* Sanity-check disk indices and block parameters. A malicious + * savestate could otherwise: + * - set InDisk to a value 0..254 (255 means ejected) that's >= + * TotalSides, causing the read path to index past the + * 8-element diskdata[] array and dereference an arbitrary + * pointer-shaped 8 bytes (heap-read primitive); + * - set mapperFDS_block to an out-of-enum value (no current + * case crashes but better to clamp); + * - set mapperFDS_blockstart + mapperFDS_diskaddr beyond 65500 + * (BYTES_PER_SIDE), causing GET_FDS_DISK() to OOB-read up + * to 65 KB past the disk buffer. */ + if (InDisk != 255 && InDisk >= TotalSides) + InDisk = 255; /* eject */ + if (SelectDisk >= TotalSides) + SelectDisk = 0; + if (mapperFDS_block > DSK_FILEDATA) /* enum values 0..4 are valid */ + mapperFDS_block = DSK_INIT; + /* BYTES_PER_SIDE is 65500. The read/write sites compute + * (blockstart + diskaddr) as the offset into a 65500-byte buffer + * after gating on diskaddr < blocklen. Clamp each component such + * that any allowed (blockstart + diskaddr) stays within the buffer: + * - blockstart < BYTES_PER_SIDE (so the base is in-range) + * - diskaddr fits in the remaining space, capped by blocklen anyway + * - blocklen capped to BYTES_PER_SIDE - blockstart so the gating + * check 'diskaddr < blocklen' is sufficient. */ + if (mapperFDS_blockstart >= BYTES_PER_SIDE) + mapperFDS_blockstart = 0; + if (mapperFDS_blocklen > BYTES_PER_SIDE - mapperFDS_blockstart) + mapperFDS_blocklen = BYTES_PER_SIDE - mapperFDS_blockstart; + if (mapperFDS_diskaddr > mapperFDS_blocklen) + mapperFDS_diskaddr = mapperFDS_blocklen; + setmirror(((FDSRegs[5] & 8) >> 3) ^ 1); if (version >= 9810) @@ -577,6 +609,14 @@ static int SubLoad(FCEUFILE *fp) { struct md5_context md5; uint8 header[16]; int x; + uint64 fsize = FCEU_fgetsize(fp); + + /* Reject files too short to contain a 16-byte header. Otherwise the + * subsequent FCEU_fread leaves header[] partially uninitialised, and + * the magic-string memcmps and side-count read at header[4] could + * spuriously succeed on stack garbage. */ + if (fsize < 16) + return(0); FCEU_fread(header, 16, 1, fp); @@ -679,6 +719,19 @@ int FDSLoad(const char *name, FCEUFILE *fp) { for (x = 0; x < TotalSides; x++) { diskdatao[x] = (uint8*)FCEU_malloc(65500); + if (!diskdatao[x]) { + int y; + for (y = 0; y < x; y++) { + free(diskdatao[y]); + diskdatao[y] = NULL; + } + if (FDSBIOS) + free(FDSBIOS); + FDSBIOS = NULL; + free(FDSROM); + FDSROM = NULL; + return(0); + } memcpy(diskdatao[x], diskdata[x], 65500); } @@ -705,22 +758,22 @@ int FDSLoad(const char *name, FCEUFILE *fp) { AddExState(&FDSRegs[3], 1, 0, "REG4"); AddExState(&FDSRegs[4], 1, 0, "REG5"); AddExState(&FDSRegs[5], 1, 0, "REG6"); - AddExState(&IRQCount, 4 | FCEUSTATE_RLSB, 1, "IRQC"); - AddExState(&IRQLatch, 4 | FCEUSTATE_RLSB, 1, "IQL1"); + AddExState(&IRQCount, 4, 1, "IRQC"); + AddExState(&IRQLatch, 4, 1, "IQL1"); AddExState(&IRQa, 1, 0, "IRQA"); AddExState(&writeskip, 1, 0, "WSKI"); - AddExState(&DiskPtr, 4 | FCEUSTATE_RLSB, 1, "DPTR"); - AddExState(&DiskSeekIRQ, 4 | FCEUSTATE_RLSB, 1, "DSIR"); + AddExState(&DiskPtr, 4, 1, "DPTR"); + AddExState(&DiskSeekIRQ, 4, 1, "DSIR"); AddExState(&SelectDisk, 1, 0, "SELD"); AddExState(&InDisk, 1, 0, "INDI"); AddExState(&DiskWritten, 1, 0, "DSKW"); AddExState(&mapperFDS_control, 1, 0, "CTRG"); - AddExState(&mapperFDS_filesize, 2 | FCEUSTATE_RLSB, 1, "FLSZ"); + AddExState(&mapperFDS_filesize, 2, 1, "FLSZ"); AddExState(&mapperFDS_block, 1, 0, "BLCK"); - AddExState(&mapperFDS_blockstart, 2 | FCEUSTATE_RLSB, 1, "BLKS"); - AddExState(&mapperFDS_blocklen, 2 | FCEUSTATE_RLSB, 1, "BLKL"); - AddExState(&mapperFDS_diskaddr, 2 | FCEUSTATE_RLSB, 1, "DADR"); + AddExState(&mapperFDS_blockstart, 2, 1, "BLKS"); + AddExState(&mapperFDS_blocklen, 2, 1, "BLKL"); + AddExState(&mapperFDS_diskaddr, 2, 1, "DADR"); AddExState(&mapperFDS_diskaccess, 1, 0, "DACC"); CHRRAMSize = 8192; diff --git a/src/file.c b/src/file.c index 30a3898..2c477b3 100644 --- a/src/file.c +++ b/src/file.c @@ -40,23 +40,30 @@ static MEMWRAP *MakeMemWrap(RFILE *tz) { MEMWRAP *tmp = NULL; + int64_t fsize; + + filestream_seek(tz, 0, RETRO_VFS_SEEK_POSITION_END); + fsize = filestream_tell(tz); + filestream_seek(tz, 0, RETRO_VFS_SEEK_POSITION_START); + /* filestream_tell may return -1 on failure; reject before passing the + * value (cast to a huge unsigned size) to malloc. */ + if (fsize < 0) + return NULL; if (!(tmp = (MEMWRAP*)FCEU_malloc(sizeof(MEMWRAP)))) goto doret; tmp->location = 0; + tmp->size = (size_t)fsize; - filestream_seek(tz, 0, RETRO_VFS_SEEK_POSITION_END); - tmp->size = filestream_tell(tz); - filestream_seek(tz, 0, RETRO_VFS_SEEK_POSITION_START); - - if (!(tmp->data_int = (uint8*)FCEU_malloc(tmp->size))) + if (tmp->size && !(tmp->data_int = (uint8*)FCEU_malloc(tmp->size))) { free(tmp); tmp = NULL; goto doret; } - filestream_read(tz, tmp->data_int, tmp->size); + if (tmp->size) + filestream_read(tz, tmp->data_int, tmp->size); tmp->data = tmp->data_int; doret: @@ -82,8 +89,19 @@ FCEUFILE * FCEU_fopen(const char *path, const uint8 *buffer, size_t bufsize) { FCEUFILE *fceufp = (FCEUFILE*)malloc(sizeof(FCEUFILE)); + if (!fceufp) + return NULL; + fceufp->fp = NULL; + if (buffer) + { fceufp->fp = MakeMemWrapBuffer(buffer, bufsize); + if (!fceufp->fp) + { + free(fceufp); + return NULL; + } + } else { RFILE *t = NULL; @@ -101,6 +119,11 @@ FCEUFILE * FCEU_fopen(const char *path, const uint8 *buffer, size_t bufsize) fceufp->fp = MakeMemWrap(t); filestream_close(t); + if (!fceufp->fp) + { + free(fceufp); + return NULL; + } } return fceufp; } diff --git a/src/general.c b/src/general.c index 3578ae1..1eca210 100644 --- a/src/general.c +++ b/src/general.c @@ -85,11 +85,21 @@ uint32 uppow2(uint32 n) { int x; + if (n == 0) + return 0; + + /* If n already has bit 31 set and isn't exactly 0x80000000, the next + * power of two would be 1u << 32 which is undefined; clamp to 0x80000000 + * instead so callers don't end up with a wrap-to-0 allocation that's + * smaller than the requested size. */ + if (n > 0x80000000u) + return 0x80000000u; + for (x = 31; x >= 0; x--) - if (n & (1 << x)) + if (n & (1u << x)) { - if ((1 << x) != n) - return(1 << (x + 1)); + if ((1u << x) != n) + return(1u << (x + 1)); break; } return n; diff --git a/src/ines.c b/src/ines.c index 212b6c6..5f1d807 100644 --- a/src/ines.c +++ b/src/ines.c @@ -1076,11 +1076,51 @@ static void iNES_read_header_info(void) { if (head.PRGRAM_size & 0xF0) iNESCart.PRGRamSaveSize = 64 << ((head.PRGRAM_size >> 4) & 0x0F); if (head.CHRRAM_size & 0x0F) iNESCart.CHRRamSize = 64 << ((head.CHRRAM_size >> 0) & 0x0F); if (head.CHRRAM_size & 0xF0) iNESCart.CHRRamSaveSize = 64 << ((head.CHRRAM_size >> 4) & 0x0F); - iNESCart.PRGRomSize = ROM_size >=0xF00? (pow(2, head.ROM_size >>2)*((head.ROM_size &3)*2+1)): (ROM_size*0x4000); - iNESCart.CHRRomSize =VROM_size >=0xF00? (pow(2, head.VROM_size>>2)*((head.VROM_size&3)*2+1)): (VROM_size*0x2000);; + /* iNES 2.0 exponent encoding: when the 12-bit count >= 0xF00, the byte + * encodes (multiplier * 2^exponent) where exponent = byte>>2 (0..63) and + * multiplier = (byte&3)*2+1. Cap exponent so the result stays well + * within uint32 (and a sane size); otherwise pow(2, 63) overflows + * uint32 implicitly with undefined results, and downstream uppow2() + * truncates back to a small allocation, leading to a heap overflow on + * the subsequent fread. Cap at 30 so the maximum is 7 << 30 = ~7 GiB + * which still fits in uint32 (truncated to ~3 GiB) but is far above + * any real cart and gets caught by sane validation. We additionally + * clamp the final value to a safe ceiling. */ + { + uint32 exp_prg = head.ROM_size >> 2; + uint32 exp_chr = head.VROM_size >> 2; + uint32 prg, chr; + if (exp_prg > 30) exp_prg = 30; + if (exp_chr > 30) exp_chr = 30; + prg = ROM_size >= 0xF00 ? ((uint32)1 << exp_prg) * ((head.ROM_size & 3) * 2 + 1) : (ROM_size * 0x4000); + chr = VROM_size >= 0xF00 ? ((uint32)1 << exp_chr) * ((head.VROM_size & 3) * 2 + 1) : (VROM_size * 0x2000); + /* Cap below 2 GiB so the value fits comfortably in the + * downstream int PRGRomSize / CHRRomSize fields without + * going negative, and so uppow2 returns a finite power-of-two + * within int range. Anything larger is bogus anyway. */ + if (prg > 0x40000000u) prg = 0x40000000u; + if (chr > 0x40000000u) chr = 0x40000000u; + iNESCart.PRGRomSize = (int)prg; + iNESCart.CHRRomSize = (int)chr; + } iNESCart.miscROMNumber =head.MiscRoms; - iNESCart.miscROMSize =iNESCart.miscROMNumber? (iNESCart.totalFileSize -16 -(head.ROM_type &4? 512: 0) -iNESCart.PRGRomSize -iNESCart.CHRRomSize): 0; - if (iNESCart.miscROMSize &0x8000000) iNESCart.miscROMSize =0; + if (iNESCart.miscROMNumber) { + /* Compute miscROMSize as int64 to avoid silent wraparound when + * the (attacker-controlled) PRGRomSize / CHRRomSize fields + * exceed the file size. Reject negative or absurdly large + * results rather than passing a wrapped value to malloc. */ + int64 misc = (int64)iNESCart.totalFileSize + - 16 + - ((head.ROM_type & 4) ? 512 : 0) + - (int64)iNESCart.PRGRomSize + - (int64)iNESCart.CHRRomSize; + if (misc <= 0 || misc > 0x8000000) /* > 128 MiB is suspect */ + iNESCart.miscROMSize = 0; + else + iNESCart.miscROMSize = (int)misc; + } else { + iNESCart.miscROMSize = 0; + } } else { iNESCart.submapper = iNESCart.miscROMNumber = iNESCart.miscROMSize = 0; iNESCart.PRGRomSize =ROM_size*0x4000; diff --git a/src/input.c b/src/input.c index fa52f8c..36f48b6 100644 --- a/src/input.c +++ b/src/input.c @@ -187,6 +187,10 @@ static uint8 FP_FASTAPASS(1) ReadGP(int w) { } static void FP_FASTAPASS(3) UpdateGP(int w, void *data, int arg) { + /* JSReturn is constructed by the libretro frontend as a uint32 with + * player N at bits (N<<3)..(N<<3)+7. Reading via host-endian uint32 + * cast and shifting gives the correct player byte regardless of host + * endianness, because the writer used the matching shift. */ uint32 *ptr = (uint32*)data; if (!w) { joy[0] = *(uint32*)ptr; diff --git a/src/input/bworld.c b/src/input/bworld.c index 92a15c0..90b1669 100644 --- a/src/input/bworld.c +++ b/src/input/bworld.c @@ -48,10 +48,19 @@ static void FP_FASTAPASS(1) Write(uint8 V) { static void FP_FASTAPASS(2) Update(void *data, int arg) { if (*(uint8*)data) { + size_t i; + const uint8 *src = (uint8*)data + 1; *(uint8*)data = 0; seq = ptr = 0; have = 1; - strcpy((char*)bdata, (const char*)((uint8*)data + 1)); + /* bdata is 20 bytes total; bytes 13..19 are filled by the + * memcpy below. Copy at most 13 bytes from `src`, ensuring + * NUL termination at index 13 or earlier. */ + for (i = 0; i < 13; i++) { + if (!src[i]) break; + bdata[i] = src[i]; + } + bdata[i] = 0; memcpy((char*)&bdata[13], "SUNSOFT", 7); } } diff --git a/src/nsf.c b/src/nsf.c index a18c694..c39dade 100644 --- a/src/nsf.c +++ b/src/nsf.c @@ -135,9 +135,14 @@ static INLINE void BANKSET(uint32 A, uint32 bank) { int NSFLoad(FCEUFILE *fp) { int x; + uint64 fsize; FCEU_fseek(fp, 0, SEEK_SET); - FCEU_fread(&NSFHeader, 1, 0x80, fp); + /* NSFHeader is file-scope static; if a previous successful load left + * it populated, a shorter file that doesn't fully overwrite it could + * bypass the ID memcmp on stale data. Validate read length. */ + if (FCEU_fread(&NSFHeader, 1, 0x80, fp) != 0x80) + return 0; if (memcmp(NSFHeader.ID, "NESM\x1a", 5)) return 0; NSFHeader.SongName[31] = NSFHeader.Artist[31] = NSFHeader.Copyright[31] = 0; @@ -155,10 +160,32 @@ int NSFLoad(FCEUFILE *fp) { PlayAddr = NSFHeader.PlayAddressLow; PlayAddr |= NSFHeader.PlayAddressHigh << 8; - NSFSize = FCEU_fgetsize(fp) - 0x80; + /* Validate that there is data past the 0x80-byte header before + * subtracting; otherwise NSFSize underflows uint64 and propagates a + * huge size into NSFMaxBank/uppow2/FCEU_malloc, leading to a heap + * overflow on the subsequent fread. Also cap at a sane ceiling. */ + fsize = FCEU_fgetsize(fp); + if (fsize <= 0x80) { + FCEUD_PrintError("NSF file truncated (no song data after header)."); + return(0); + } + NSFSize = fsize - 0x80; + /* Ceiling: 16 MiB of song data is far beyond reality */ + if (NSFSize > (16u << 20)) { + FCEUD_PrintError("NSF song data unreasonably large."); + return(0); + } NSFMaxBank = ((NSFSize + (LoadAddr & 0xfff) + 4095) / 4096); NSFMaxBank = uppow2(NSFMaxBank); + /* Cap at 2^19 so NSFMaxBank * 4096 fits in signed int (the type + * of NSFMaxBank). 2^19 * 4096 = 2 GiB which is already absurd for + * an NSF file; the previous cap of 2^20 produced 4 GiB which + * overflows signed int and is undefined behaviour. */ + if (!NSFMaxBank || NSFMaxBank > (1u << 19)) { + FCEUD_PrintError("NSF bank count out of range."); + return(0); + } if (!(NSFDATA = (uint8*)FCEU_malloc(NSFMaxBank * 4096))) return 0; diff --git a/src/state.c b/src/state.c index 5dcc48b..1cd82ea 100644 --- a/src/state.c +++ b/src/state.c @@ -267,7 +267,7 @@ void FCEUSS_Save_Mem(void) totalsize += WriteStateChunk(mem, 0x10, SFMDATA); - if (SPreSave) + if (SPostSave) SPostSave(); memstream_seek(mem, 4, SEEK_SET); @@ -326,6 +326,22 @@ void AddExState(void *v, uint32 s, int type, char *desc) { /* prevent adding a terminator to the list if a NULL pointer was provided */ if (v == NULL) return; + /* Need at least one slot for the new entry plus one for the terminator. + * If we're already at the second-to-last slot (62), we can write the + * entry there and the terminator at slot 63, but no further entries + * may be added. Beyond that, drop the entry to avoid silently + * overwriting the previous one with the terminator. */ + if (SFEXINDEX >= (int)(sizeof(SFMDATA) / sizeof(SFMDATA[0])) - 1) + { + static int warned = 0; + if (!warned) + { + FCEU_PrintError(" SFMDATA[] full; some state entries dropped (\"%s\").\n", + desc ? desc : "?"); + warned = 1; + } + return; + } memset(SFMDATA[SFEXINDEX].desc, 0, sizeof(SFMDATA[SFEXINDEX].desc)); if (desc) strncpy(SFMDATA[SFEXINDEX].desc, desc, sizeof(SFMDATA[SFEXINDEX].desc)); @@ -333,8 +349,7 @@ void AddExState(void *v, uint32 s, int type, char *desc) SFMDATA[SFEXINDEX].s = s; if (type) SFMDATA[SFEXINDEX].s |= RLSB; - if (SFEXINDEX < 63) - SFEXINDEX++; + SFEXINDEX++; SFMDATA[SFEXINDEX].v = 0; /* End marker. */ } diff --git a/src/unif.c b/src/unif.c index 4d47250..41223e2 100644 --- a/src/unif.c +++ b/src/unif.c @@ -93,6 +93,10 @@ static int FixRomSize(uint32 size, uint32 minimum) { if (size < minimum) return minimum; + /* Cap input to avoid the infinite loop where x doubles past 0x80000000 + * and wraps to 0, never reaching size. */ + if (size > 0x80000000u) + return 0x80000000u; while (x < size) x <<= 1; return x; @@ -198,6 +202,8 @@ static int NAME(FCEUFILE *fp) { if (!GameInfo->name) { GameInfo->name = malloc(strlen(namebuf) + 1); + if (!GameInfo->name) + return(0); strcpy((char*)GameInfo->name, namebuf); } return(1); @@ -229,7 +235,11 @@ static int DINF(FCEUFILE *fp) { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; - FCEU_printf(" Dumped on: %s %d, %d\n", months[(m - 1) % 12], d, y); + /* m comes from a (possibly malicious) .unf file. If m is 0 or + * out of range, '(m - 1) % 12' would underflow / wrap signed and + * could index off the months[] array. Clamp into [1..12] first. */ + unsigned mi = (m >= 1 && m <= 12) ? (unsigned)(m - 1) : 0; + FCEU_printf(" Dumped on: %s %d, %d\n", months[mi], d, y); } return(1); } @@ -286,11 +296,18 @@ static int EnableBattery(FCEUFILE *fp) { } static int LoadPRG(FCEUFILE *fp) { - int z, t; + int z; + uint32 t; z = uchead.ID[3] - '0'; if (z < 0 || z > 15) return(0); + /* uchead.info is uint32 from the file; reject sizes that would either + * overflow when added below, or that are obviously bogus. The largest + * legitimate single PRG chunk is in the low MiB. Cap at 64 MiB which + * is well above reality but still safe. */ + if (uchead.info == 0 || uchead.info > (64u << 20)) + return(0); FCEU_printf(" PRG ROM %d size: %d\n", z, (int)uchead.info); if (malloced[z]) free(malloced[z]); @@ -298,7 +315,6 @@ static int LoadPRG(FCEUFILE *fp) { if (!(malloced[z] = (uint8*)FCEU_malloc(t))) return(0); mallocedsizes[z] = t; - memset(malloced[z] + uchead.info, 0xFF, t - uchead.info); if (FCEU_fread(malloced[z], 1, uchead.info, fp) != uchead.info) { FCEU_printf("Read Error!\n"); return(0); @@ -312,6 +328,14 @@ static int LoadPRG(FCEUFILE *fp) { } static int SetBoardName(FCEUFILE *fp) { + /* The subsequent 4-byte memcmp()s for "NES-", "UNL-", "HVC-", "BTL-", + * "BMC-" require the boardname buffer to be at least 4 bytes long. + * A malformed UNIF file can declare a MAPR chunk with size < 4, which + * would otherwise produce a heap out-of-bounds read. */ + if (uchead.info < 4) { + FCEU_PrintError(" MAPR chunk too small (%u bytes); ignoring.\n", (unsigned)uchead.info); + return 0; + } if (!(boardname = (uint8*)FCEU_malloc(uchead.info + 1))) return(0); FCEU_fread(boardname, 1, uchead.info, fp); @@ -329,10 +353,13 @@ static int SetBoardName(FCEUFILE *fp) { } static int LoadCHR(FCEUFILE *fp) { - int z, t; + int z; + uint32 t; z = uchead.ID[3] - '0'; if (z < 0 || z > 15) return(0); + if (uchead.info == 0 || uchead.info > (64u << 20)) + return(0); FCEU_printf(" CHR ROM %d size: %d\n", z, (int)uchead.info); if (malloced[16 + z]) free(malloced[16 + z]); @@ -340,7 +367,6 @@ static int LoadCHR(FCEUFILE *fp) { if (!(malloced[16 + z] = (uint8*)FCEU_malloc(t))) return(0); mallocedsizes[16 + z] = t; - memset(malloced[16 + z] + uchead.info, 0xFF, t - uchead.info); if (FCEU_fread(malloced[16 + z], 1, uchead.info, fp) != uchead.info) { FCEU_printf("Read Error!\n"); return(0); @@ -750,7 +776,12 @@ int UNIFLoad(const char *name, FCEUFILE *fp) { int x = 0; FCEU_fseek(fp, 0, SEEK_SET); - FCEU_fread(&unhead, 1, 4, fp); + /* unhead is a file-scope static; if FCEU_fread reads fewer than 4 bytes + * (e.g. on a malformed empty file), the static would retain whatever + * was left from a previous successful load and the memcmp could + * spuriously succeed. Verify the read first. */ + if (FCEU_fread(&unhead, 1, 4, fp) != 4) + return 0; if (memcmp(&unhead, "UNIF", 4)) return 0; diff --git a/src/video.c b/src/video.c index 325ff0a..c1e4347 100644 --- a/src/video.c +++ b/src/video.c @@ -58,7 +58,12 @@ int FCEU_InitVirtualVideo(void) XDBuf = (uint8*)(FCEU_malloc(256 * (256 + extrascanlines + 8))); if (!XBuf || !XDBuf) + { + /* Free whatever did allocate to avoid leaking on retry */ + if (XBuf) { free(XBuf); XBuf = NULL; } + if (XDBuf) { free(XDBuf); XDBuf = NULL; } return 0; + } memset(XBuf, 128, 256 * (256 + extrascanlines + 8)); memset(XDBuf, 0, 256 * (256 + extrascanlines + 8)); @@ -93,7 +98,11 @@ void FCEU_DispMessage(enum retro_log_level level, unsigned duration, const char return; va_start(ap, format); - vsprintf(msg, format, ap); + /* Use vsnprintf to bound the write to msg[]. The previous vsprintf + * had no length cap; format strings are currently all literals with + * controlled args, but a future caller passing a long ROM name into + * a %s format would overrun the 512-byte buffer. */ + vsnprintf(msg, sizeof(msg), format, ap); va_end(ap); FCEUD_DispMessage(level, duration, msg);