Files
ci-libretro-fceumm/src/fds.c

819 lines
20 KiB
C
Raw Normal View History

/* 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
*/
/* dec-12-19 */
/* 4024(w), 4025(w), 4031(r) by dink(fbneo) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "fceu-types.h"
#include "x6502.h"
#include "fceu.h"
#include "fds.h"
#include "fds_apu.h"
#include "sound.h"
#include "general.h"
#include "state.h"
#include "file.h"
2014-03-30 22:29:30 +02:00
#include "fceu-memory.h"
#include "cart.h"
#include "md5.h"
2017-10-15 03:13:11 +08:00
/* TODO: Add code to put a delay in between the time a disk is inserted
* and the when it can be successfully read/written to. This should
* prevent writes to wrong places OR add code to prevent disk ejects
* when the virtual motor is on(mmm...virtual motor).
*/
static DECLFR(FDSRead4030);
static DECLFR(FDSRead4031);
static DECLFR(FDSRead4032);
static DECLFR(FDSRead4033);
static DECLFW(FDSWrite);
static void FDSInit(void);
static void FDSClose(void);
static void FP_FASTAPASS(1) FDSFix(int a);
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
static uint8_t FDSRegs[6];
static int32_t IRQLatch, IRQCount;
static uint8_t IRQa;
static uint8_t *FDSROM = NULL;
static uint32_t FDSROMSize = 0;
static uint8_t *FDSRAM = NULL;
static uint32_t FDSRAMSize;
static uint8_t *FDSBIOS = NULL;
static uint32_t FDSBIOSsize;
static uint8_t *CHRRAM = NULL;
static uint32_t CHRRAMSize;
/* Original disk data backup, to help in creating save states. */
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
static uint8_t *diskdatao[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
static uint8_t *diskdata[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
static uint32_t TotalSides;
static uint8_t DiskWritten = 0; /* Set to 1 if disk was written to. */
static uint8_t writeskip;
static int32_t DiskPtr;
static int32_t DiskSeekIRQ;
static uint8_t SelectDisk, InDisk;
enum FDS_DiskBlockIDs {
DSK_INIT = 0,
DSK_VOLUME,
DSK_FILECNT,
DSK_FILEHDR,
DSK_FILEDATA
};
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
static uint8_t mapperFDS_control; /* 4025(w) control register */
static uint16_t mapperFDS_filesize; /* size of file being read/written */
static uint8_t mapperFDS_block; /* block-id of current block */
static uint16_t mapperFDS_blockstart; /* start-address of current block */
static uint16_t mapperFDS_blocklen; /* length of current block */
static uint16_t mapperFDS_diskaddr; /* current address relative to blockstart */
static uint8_t mapperFDS_diskaccess; /* disk needs to be accessed at least once before writing */
#define GET_FDS_DISK() (diskdata[InDisk][mapperFDS_blockstart + mapperFDS_diskaddr])
#define FDS_DISK_INSERTED (InDisk != 255)
#define DC_INC 1
#define BYTES_PER_SIDE 65500
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint8_t *FDSROM_ptr(void) {
return (FDSROM);
}
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint32_t FDSROM_size(void) {
return (FDSROMSize);
}
void FDSGI(int h) {
switch (h) {
case GI_CLOSE: FDSClose(); break;
case GI_POWER: FDSInit(); break;
}
}
static void FDSStateRestore(int version) {
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t x;
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
/* 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)
for (x = 0; x < TotalSides; x++) {
int b;
for (b = 0; b < 65500; b++)
diskdata[x][b] ^= diskdatao[x][b];
}
}
static void FDSInit(void) {
memset(FDSRegs, 0, sizeof(FDSRegs));
writeskip = DiskPtr = DiskSeekIRQ = 0;
setmirror(1);
2017-10-15 03:13:11 +08:00
setprg8(0xE000, 0); /* BIOS */
setprg32r(1, 0x6000, 0); /* 32KB RAM */
setchr8(0); /* 8KB CHR RAM */
MapIRQHook = FDSFix;
GameStateRestore = FDSStateRestore;
SetReadHandler(0x4030, 0x4030, FDSRead4030);
SetReadHandler(0x4031, 0x4031, FDSRead4031);
SetReadHandler(0x4032, 0x4032, FDSRead4032);
SetReadHandler(0x4033, 0x4033, FDSRead4033);
SetWriteHandler(0x4020, 0x4025, FDSWrite);
SetWriteHandler(0x6000, 0xDFFF, CartBW);
SetReadHandler(0x6000, 0xFFFF, CartBR);
IRQCount = IRQLatch = IRQa = 0;
FDSSoundReset();
InDisk = 0;
SelectDisk = 0;
mapperFDS_control = 0;
mapperFDS_filesize = 0;
mapperFDS_block = 0;
mapperFDS_blockstart = 0;
mapperFDS_blocklen = 0;
mapperFDS_diskaddr = 0;
mapperFDS_diskaccess = 0;
}
void FCEU_FDSInsert(int oride) {
if (InDisk == 255) {
FCEU_DispMessage(RETRO_LOG_INFO, 2000, "Disk %d of %d Side %s Inserted",
1 + (SelectDisk >> 1), (TotalSides + 1) >> 1, (SelectDisk & 1) ? "B" : "A");
InDisk = SelectDisk;
} else {
FCEU_DispMessage(RETRO_LOG_INFO, 2000, "Disk %d of %d Side %s Ejected",
1 + (SelectDisk >> 1), (TotalSides + 1) >> 1, (SelectDisk & 1) ? "B" : "A");
InDisk = 255;
}
}
void FCEU_FDSEject(void) {
InDisk = 255;
}
void FCEU_FDSSelect(void) {
if (InDisk != 255) {
FCEUD_DispMessage(RETRO_LOG_WARN, 2000, "Eject disk before selecting");
return;
}
SelectDisk = ((SelectDisk + 1) % TotalSides) & 3;
FCEU_DispMessage(RETRO_LOG_INFO, 2000, "Disk %d of %d Side %s Selected",
1 + (SelectDisk >> 1), (TotalSides + 1) >> 1, (SelectDisk & 1) ? "B" : "A");
}
/* 2018/12/15 - update irq timings */
static void FP_FASTAPASS(1) FDSFix(int a) {
if ((IRQa & 2) && (FDSRegs[3] & 0x1)) {
if (IRQCount <= 0) {
if (!(IRQa & 1))
IRQa &= ~2; /* does not clear latch, fix Druid */
IRQCount = IRQLatch;
X6502_IRQBegin(FCEU_IQEXT);
} else {
IRQCount -= a;
}
}
if (DiskSeekIRQ > 0) {
DiskSeekIRQ -= a;
if (DiskSeekIRQ <= 0) {
if (FDSRegs[5] & 0x80) {
X6502_IRQBegin(FCEU_IQEXT2);
}
}
}
}
static DECLFR(FDSRead4030) {
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint8_t ret = 0;
/* Cheap hack. */
if (X.IRQlow & FCEU_IQEXT) ret |= 1;
if (X.IRQlow & FCEU_IQEXT2) ret |= 2;
{
X6502_IRQEnd(FCEU_IQEXT);
X6502_IRQEnd(FCEU_IQEXT2);
}
return ret;
}
static DECLFR(FDSRead4031) {
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint8_t ret = 0xff;
if (FDS_DISK_INSERTED && mapperFDS_control & 0x04) {
mapperFDS_diskaccess = 1;
ret = 0;
switch (mapperFDS_block) {
case DSK_FILEHDR:
if (mapperFDS_diskaddr < mapperFDS_blocklen) {
ret = GET_FDS_DISK();
switch (mapperFDS_diskaddr) {
case 13:
mapperFDS_filesize = ret;
break;
case 14:
mapperFDS_filesize |= ret << 8;
break;
}
mapperFDS_diskaddr++;
}
break;
default:
if (mapperFDS_diskaddr < mapperFDS_blocklen) {
ret = GET_FDS_DISK();
mapperFDS_diskaddr++;
}
break;
}
DiskSeekIRQ = 150;
X6502_IRQEnd(FCEU_IQEXT2);
}
return ret;
}
static DECLFR(FDSRead4032) {
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint8_t ret;
ret = X.DB & ~7;
if (InDisk == 255)
ret |= 5;
if (InDisk == 255 || !(FDSRegs[5] & 1) || (FDSRegs[5] & 2))
ret |= 2;
return ret;
}
static DECLFR(FDSRead4033) {
2017-10-15 03:13:11 +08:00
return 0x80; /* battery */
}
static DECLFW(FDSWrite) {
switch (A) {
case 0x4020:
IRQLatch &= 0xFF00;
IRQLatch |= V;
break;
case 0x4021:
IRQLatch &= 0xFF;
IRQLatch |= V << 8;
break;
case 0x4022:
if (FDSRegs[3] & 0x1) {
IRQa = (V & 0x3);
if (IRQa & 2) {
IRQCount = IRQLatch;
} else {
X6502_IRQEnd(FCEU_IQEXT);
X6502_IRQEnd(FCEU_IQEXT2);
}
}
break;
case 0x4023:
if (!(V & 1)) {
X6502_IRQEnd(FCEU_IQEXT);
X6502_IRQEnd(FCEU_IQEXT2);
}
2020-10-21 20:55:08 +08:00
break;
case 0x4024:
if (FDS_DISK_INSERTED && ~mapperFDS_control & 0x04) {
if (mapperFDS_diskaccess == 0) {
mapperFDS_diskaccess = 1;
break;
}
switch (mapperFDS_block) {
case DSK_FILEHDR:
if (mapperFDS_diskaddr < mapperFDS_blocklen) {
GET_FDS_DISK() = V;
switch (mapperFDS_diskaddr) {
case 13: mapperFDS_filesize = V; break;
case 14:
mapperFDS_filesize |= V << 8;
break;
}
mapperFDS_diskaddr++;
}
break;
default:
if (mapperFDS_diskaddr < mapperFDS_blocklen) {
GET_FDS_DISK() = V;
mapperFDS_diskaddr++;
}
break;
}
}
break;
case 0x4025:
X6502_IRQEnd(FCEU_IQEXT2);
if (FDS_DISK_INSERTED) {
if (V & 0x40 && ~mapperFDS_control & 0x40) {
mapperFDS_diskaccess = 0;
DiskSeekIRQ = 150;
/* blockstart - address of block on disk
* diskaddr - address relative to blockstart
* _block -> _blockID ?
*/
mapperFDS_blockstart += mapperFDS_diskaddr;
mapperFDS_diskaddr = 0;
mapperFDS_block++;
if (mapperFDS_block > DSK_FILEDATA)
mapperFDS_block = DSK_FILEHDR;
switch (mapperFDS_block) {
case DSK_VOLUME:
mapperFDS_blocklen = 0x38;
break;
case DSK_FILECNT:
mapperFDS_blocklen = 0x02;
break;
case DSK_FILEHDR:
mapperFDS_blocklen = 0x10;
break;
case DSK_FILEDATA: /* <blockid><filedata> */
mapperFDS_blocklen = 0x01 + mapperFDS_filesize;
break;
}
}
if (V & 0x02) { /* transfer reset */
mapperFDS_block = DSK_INIT;
mapperFDS_blockstart = 0;
mapperFDS_blocklen = 0;
mapperFDS_diskaddr = 0;
DiskSeekIRQ = 150;
}
if (V & 0x40) { /* turn on motor */
DiskSeekIRQ = 150;
}
}
mapperFDS_control = V;
setmirror(((V >> 3) & 1) ^ 1);
break;
}
FDSRegs[A & 7] = V;
}
struct codes_t {
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint8_t code;
char *name;
};
static const struct codes_t list[] = {
{ 0x01, "Nintendo" },
{ 0x02, "Rocket Games" },
{ 0x08, "Capcom" },
{ 0x09, "Hot B Co." },
{ 0x0A, "Jaleco" },
{ 0x0B, "Coconuts Japan" },
{ 0x0C, "Coconuts Japan/G.X.Media" },
{ 0x13, "Electronic Arts Japan" },
{ 0x18, "Hudson Soft Japan" },
{ 0x19, "S.C.P." },
{ 0x1A, "Yonoman" },
{ 0x20, "Destination Software" },
{ 0x22, "VR 1 Japan" },
{ 0x25, "San-X" },
{ 0x28, "Kemco Japan" },
{ 0x29, "Seta" },
{ 0x36, "Codemasters" },
{ 0x37, "GAGA Communications" },
{ 0x38, "Laguna" },
{ 0x39, "Telstar Fun and Games" },
{ 0x41, "Ubi Soft Entertainment" },
{ 0x42, "Sunsoft" },
{ 0x47, "Spectrum Holobyte" },
{ 0x49, "Irem" },
{ 0x4A, "Gakken" },
{ 0x4D, "Malibu Games" },
{ 0x4F, "Eidos/U.S. Gold" },
{ 0x50, "Absolute Entertainment" },
{ 0x51, "Acclaim" },
{ 0x52, "Activision" },
{ 0x53, "American Sammy Corp." },
{ 0x54, "Take 2 Interactive" },
{ 0x55, "Hi Tech" },
{ 0x56, "LJN LTD." },
{ 0x58, "Mattel" },
{ 0x5A, "Mindscape/Red Orb Ent." },
{ 0x5C, "Taxan" },
{ 0x5D, "Midway" },
{ 0x5F, "American Softworks" },
{ 0x60, "Titus Interactive Studios" },
{ 0x61, "Virgin Interactive" },
{ 0x62, "Maxis" },
{ 0x64, "LucasArts Entertainment" },
{ 0x67, "Ocean" },
{ 0x69, "Electronic Arts" },
{ 0x6E, "Elite Systems Ltd." },
{ 0x6F, "Electro Brain" },
{ 0x70, "Infogrames" },
{ 0x71, "Interplay" },
{ 0x72, "JVC Musical Industries Inc" },
{ 0x73, "Parker Brothers" },
{ 0x75, "SCI" },
{ 0x78, "THQ" },
{ 0x79, "Accolade" },
{ 0x7A, "Triffix Ent. Inc." },
{ 0x7C, "Microprose Software" },
{ 0x7D, "Universal Interactive Studios" },
{ 0x7F, "Kemco" },
{ 0x80, "Misawa" },
{ 0x83, "LOZC" },
{ 0x8B, "Bulletproof Software" },
{ 0x8C, "Vic Tokai Inc." },
{ 0x91, "Chun Soft" },
{ 0x92, "Video System" },
{ 0x93, "BEC" },
{ 0x96, "Yonezawa/S'pal" },
{ 0x97, "Kaneko" },
{ 0x99, "Victor Interactive Software" },
{ 0x9A, "Nichibutsu/Nihon Bussan" },
{ 0x9B, "Tecmo" },
{ 0x9C, "Imagineer" },
{ 0x9F, "Nova" },
{ 0xA0, "Telenet" },
{ 0xA1, "Hori" },
{ 0xA2, "Scorpion Soft " },
{ 0xA4, "Konami" },
{ 0xA6, "Kawada" },
{ 0xA7, "Takara" },
{ 0xA8, "Royal Industries" },
{ 0xA9, "Technos Japan Corp." },
{ 0xAA, "JVC" },
{ 0xAC, "Toei Animation" },
{ 0xAD, "Toho" },
{ 0xAF, "Namco" },
{ 0xB0, "Acclaim Japan" },
{ 0xB1, "ASCII" },
{ 0xB2, "Bandai" },
{ 0xB3, "Soft Pro Inc." },
{ 0xB4, "Enix" },
{ 0xB6, "HAL Laboratory" },
{ 0xB7, "SNK" },
{ 0xB9, "Pony Canyon Hanbai" },
{ 0xBA, "Culture Brain" },
{ 0xBB, "Sunsoft" },
{ 0xBC, "Toshiba EMI" },
{ 0xBD, "Sony Imagesoft" },
{ 0xBF, "Sammy" },
{ 0xC0, "Taito" },
{ 0xC1, "Sunsoft / Ask Co., Ltd." },
{ 0xC2, "Kemco" },
{ 0xC3, "Square Soft" },
{ 0xC4, "Tokuma Shoten " },
{ 0xC5, "Data East" },
{ 0xC6, "Tonkin House" },
{ 0xC7, "East Cube" },
{ 0xC8, "Koei" },
{ 0xCA, "Konami/Palcom/Ultra" },
{ 0xCB, "Vapinc/NTVIC" },
{ 0xCC, "Use Co.,Ltd." },
{ 0xCD, "Meldac" },
{ 0xCE, "FCI/Pony Canyon" },
{ 0xCF, "Angel" },
{ 0xD1, "Sofel" },
/*{ 0xD2, "Quest" },*/
{ 0xD2, "Bothtec, Inc." },
{ 0xD3, "Sigma Enterprises" },
{ 0xD4, "Ask Kodansa" },
{ 0xD6, "Naxat" },
{ 0xD7, "Copya System" },
{ 0xD9, "Banpresto" },
{ 0xDA, "TOMY" },
/*{ 0xDB, "LJN Japan" },*/
{ 0xDB, "Hiro Co., Ltd." },
{ 0xDD, "NCS" },
{ 0xDF, "Altron Corporation" },
{ 0xE2, "Yutaka" },
{ 0xE3, "Varie" },
{ 0xE5, "Epoch" },
{ 0xE7, "Athena" },
{ 0xE8, "Asmik Ace Entertainment Inc." },
{ 0xE9, "Natsume" },
{ 0xEA, "King Records" },
{ 0xEB, "Atlus" },
{ 0xEC, "Epic/Sony Records" },
{ 0xEE, "IGS" },
{ 0xF0, "A Wave" },
{ 0 }
};
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
static const char *getManufacturer(uint8_t code)
{
int x = 0;
char *ret = "unlicensed";
while (list[x].code != 0) {
if (list[x].code == code) {
ret = list[x].name;
break;
}
x++;
}
return ret;
}
static void FreeFDSMemory(void) {
if (FDSROM)
free(FDSROM);
FDSROM = NULL;
if (FDSBIOS)
free(FDSBIOS);
FDSBIOS = NULL;
if (FDSRAM)
free(FDSRAM);
FDSRAM = NULL;
if (CHRRAM)
free(CHRRAM);
CHRRAM = NULL;
}
static int SubLoad(FCEUFILE *fp) {
struct md5_context md5;
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint8_t header[16];
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t x;
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
uint64_t fsize = FCEU_fgetsize(fp);
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
/* 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);
if (memcmp(header, "FDS\x1a", 4)) {
if (!(memcmp(header + 1, "*NINTENDO-HVC*", 14))) {
long t;
t = FCEU_fgetsize(fp);
if (t < 65500)
t = 65500;
TotalSides = t / 65500;
FCEU_fseek(fp, 0, SEEK_SET);
} else
return(0);
} else
TotalSides = header[4];
if (TotalSides > 8) TotalSides = 8;
if (TotalSides < 1) TotalSides = 1;
FDSROMSize = TotalSides * BYTES_PER_SIDE;
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
FDSROM = (uint8_t*)FCEU_malloc(FDSROMSize);
if (!FDSROM)
return (0);
for (x = 0; x < TotalSides; x++)
diskdata[x] = &FDSROM[x * BYTES_PER_SIDE];
md5_starts(&md5);
for (x = 0; x < TotalSides; x++) {
FCEU_fread(diskdata[x], 1, 65500, fp);
md5_update(&md5, diskdata[x], 65500);
}
md5_finish(&md5, GameInfo->MD5);
return(1);
}
static void PreSave(void) {
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t x;
for (x = 0; x < TotalSides; x++) {
int b;
for (b = 0; b < 65500; b++)
diskdata[x][b] ^= diskdatao[x][b];
}
}
static void PostSave(void) {
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t x;
for (x = 0; x < TotalSides; x++) {
int b;
for (b = 0; b < 65500; b++)
diskdata[x][b] ^= diskdatao[x][b];
}
}
int FDSLoad(const char *name, FCEUFILE *fp) {
FCEUFILE *zp;
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t x;
char *fn = FCEU_MakeFName(FCEUMKF_FDSROM, 0, 0);
if (!(zp = FCEU_fopen(fn, NULL, 0))) {
FCEU_PrintError("FDS BIOS ROM image missing!\n");
FCEUD_DispMessage(RETRO_LOG_ERROR, 3000, "FDS BIOS image (disksys.rom) missing");
free(fn);
return 0;
}
free(fn);
FreeFDSMemory();
ResetCartMapping();
FDSBIOSsize = 8192;
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
FDSBIOS = (uint8_t*)FCEU_gmalloc(FDSBIOSsize);
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
if (!FDSBIOS) {
FCEU_fclose(zp);
return 0;
}
SetupCartPRGMapping(0, FDSBIOS, FDSBIOSsize, 0);
if (FCEU_fread(FDSBIOS, 1, FDSBIOSsize, zp) != FDSBIOSsize) {
if (FDSBIOS)
free(FDSBIOS);
FDSBIOS = NULL;
FCEU_fclose(zp);
FCEU_PrintError("Error reading FDS BIOS ROM image.\n");
FCEUD_DispMessage(RETRO_LOG_ERROR, 3000, "Error reading FDS BIOS image (disksys.rom)");
return 0;
}
FCEU_fclose(zp);
FCEU_fseek(fp, 0, SEEK_SET);
if (!SubLoad(fp)) {
if (FDSBIOS)
free(FDSBIOS);
FDSBIOS = NULL;
return(0);
}
for (x = 0; x < TotalSides; x++) {
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
diskdatao[x] = (uint8_t*)FCEU_malloc(65500);
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 (!diskdatao[x]) {
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t y;
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
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);
}
DiskWritten = 1;
GameInfo->type = GIT_FDS;
GameInterface = FDSGI;
SelectDisk = 0;
InDisk = 255;
ResetExState(PreSave, PostSave);
FDSSoundStateAdd();
for (x = 0; x < TotalSides; x++) {
char temp[5];
core: strict-aliasing fixes, bounded string ops, drop FCEUDEF_DEBUGGER Three follow-up cleanups on top of the determinism / typedef / LE work in f2a5be3. ================================================================ Pass 1: fix strict-aliasing UB in PPU sprite buffer and FCEU_dwmemset ================================================================ Three sites were doing type-punning through uint32_t* casts that GCC flags with -Wstrict-aliasing=2: 1. ppu.c FetchSpriteData (two near-identical sites). After populating a local SPRB struct (4 bytes: ca[2], atr, x), the prior code did '*(uint32_t*)&SPRBUF[ns << 2] = *(uint32_t*)&dst' to store the struct as one 32-bit word. This violates strict-aliasing (uint8_t buffer accessed as uint32_t, struct accessed as uint32_t) and silently assumes 4-byte alignment of SPRBUF + (ns << 2). Use memcpy instead - GCC and Clang lower it to the same single 32-bit store. 2. fceu-memory.h FCEU_dwmemset macro. Same pattern every iteration: '*(uint32_t*)& (d)[_x] = c'. Replaced with memcpy(... &v, 4) in the macro body. Same generated code, now alias- and align-clean. Added #include <string.h> to fceu-memory.h itself (4 callers relied on transitive includes for memcpy). ================================================================ Pass 2: bounded string operations ================================================================ Sweep across the core and libretro driver to replace every unbounded string function with its size-aware counterpart: strcpy -> strlcpy (with explicit destination size) strncpy -> strlcpy (silent truncation -> guaranteed NUL termination and known-size semantics; behaviourally identical at every call site here because callers either pre-zeroed the buffer or treated truncation as a guaranteed terminator) strcat -> strlcpy at offset (track 'pos' across appends to avoid rescanning the buffer with strlen and to keep the bound explicit at every step) sprintf -> snprintf (with sizeof(buf) as the bound) The compat/strl.h header from libretro-common is on the include path for every translation unit; the libretro driver files were already using strlcpy via stdstring.h transitive include. Core files (ines.c, unif.c, cheat.c, nsf.c, state.c, general.c) gain explicit '#include <compat/strl.h>'. Per-file changes: general.c - FCEUI_SetBaseDirectory: strncpy + manual NUL -> strlcpy - FCEU_MakeFName: malloc(strlen+1) + strcpy -> sized strlcpy, malloc NULL-check added (was unchecked). ines.c - 'gigastr' iNES-header-warning building. Pre-existing bug fixed: every sprintf(gigastr + gigastr_len, ...) wrote at the SAME offset captured once at the top of the block, so when multiple 'tofix' bits were set, only the LAST fragment survived (each sprintf clobbered the previous). Replaced with a running 'pos' offset across snprintf and strlcpy-at-offset calls. - 6 sprintf -> snprintf, 3 strcat -> strlcpy at offset, 1 strcpy -> strlcpy. unif.c - GameInfo->name allocation: strcpy -> strlcpy with explicit size. cheat.c - FCEUI_AddCheat: malloc + strcpy -> sized strlcpy. - FCEUI_SetCheat: realloc + strcpy -> sized strlcpy. nsf.c - FCEUI_NSFGetInfo: 3x strncpy -> strlcpy. - Visualiser snbuf sprintf -> snprintf (the 16-byte buffer was technically overflowable for very large song counts). state.c - AddExState description copy: strncpy -> strlcpy. Preserves 4-byte SFORMAT-tag semantics. fds.c - Disk-tag formatting: sprintf "DDT%d" -> snprintf. boards/__serial.c - 2 sprintf -> snprintf (Windows-only SerialOpen path). drivers/libretro/libretro.c - retro_cheat_set: sprintf "N/A" + strcpy literal -> strlcpy. drivers/libretro/libretro_dipswitch.c - VS-DIP key building: sprintf -> snprintf. - core_key calloc + strcpy -> sized strlcpy. calloc NULL-check added (was unchecked - dereferencing NULL on OOM). drivers/libretro/libretro_core_options.h - values_buf assembly: single strcpy + N strcat replaced with strlcpy + strlcpy-at-offset using a running 'pos'. Each step bounded by remaining buffer space. ================================================================ Pass 3: remove FCEUDEF_DEBUGGER and the unused debugger scaffolding ================================================================ The FCEUDEF_DEBUGGER macro was never defined for libretro builds, so every block guarded by it was dead code. Removing the macro takes out: - src/debug.c (FCEUI_DumpMem, FCEUI_DumpVid, FCEUI_LoadMem, FCEUI_Disassemble, FCEUI_MemDump, FCEUI_MemSafePeek, FCEUI_MemPoke, breakpoint set/get/list, FCEUI_SetCPUCallback). Not in Makefile.common SOURCES_C, never compiled. None of the declared functions had any caller in any compiled .c file. - src/debug.h (declarations for the above). - x6502.c X6502_RunDebug. The dual-implementation pattern with a function pointer that switched between RunNormal and RunDebug is now a single direct X6502_Run. - x6502.c X6502_Debug, FCEUI_NMI, FCEUI_IRQ, FCEUI_GetIVectors. Set/get debugger hooks. No callers. - x6502.c RdMemHook, WrMemHook, XSave, debugmode. Hook scaffolding used only by RunDebug. - x6502struct.h X6502 fields preexec, CPUHook, ReadHook, WriteHook. Set only inside RunDebug, never read elsewhere. - fceuindbg variable. Set to 1 only in FCEUI_GetIVectors and X6502_RunDebug (both removed); was always 0 elsewhere, so every 'if (!fceuindbg)' check was a no-op. Removed both the variable (defined in ppu.c, declared in fceu.h) and every check site across sound.c, input.c, fds.c, nsf.c, ppu.c, mmc5.c, n106.c, BMW8544.c, and the input drivers (arkanoid, mahjong, mouse, pec586kb, powerpad, zapper). - All FCEUDEF_DEBUGGER conditional declarations from driver.h. The 'if (!fceuindbg)' checks gated side-effects (joypad-bit-counter increments, PPU register reads, etc.) so a future debugger could peek at memory without advancing the emulator state. With the debugger gone, those side-effects are now unconditional - which is what they should have been anyway in a libretro build. If a future developer needs a debugger they should resurrect this out of git history into a separate debugger-frontend project rather than re-introducing a build-time toggle that nothing in the libretro build can ever exercise. ================================================================ Build status ================================================================ Build clean on `make platform=unix` with zero errors and zero warnings. Output binary 184 bytes smaller than upstream f2a5be3 (4,388,840 -> 4,388,656). 31 files changed, 127 insertions, 863 deletions.
2026-05-04 03:23:56 +02:00
snprintf(temp, sizeof(temp), "DDT%d", x);
AddExState(diskdata[x], 65500, 0, temp);
}
AddExState(&FDSRegs[0], 1, 0, "REG1");
AddExState(&FDSRegs[1], 1, 0, "REG2");
AddExState(&FDSRegs[2], 1, 0, "REG3");
AddExState(&FDSRegs[3], 1, 0, "REG4");
AddExState(&FDSRegs[4], 1, 0, "REG5");
AddExState(&FDSRegs[5], 1, 0, "REG6");
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
AddExState(&IRQCount, 4, 1, "IRQC");
AddExState(&IRQLatch, 4, 1, "IQL1");
AddExState(&IRQa, 1, 0, "IRQA");
AddExState(&writeskip, 1, 0, "WSKI");
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
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");
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
AddExState(&mapperFDS_filesize, 2, 1, "FLSZ");
AddExState(&mapperFDS_block, 1, 0, "BLCK");
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
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;
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
CHRRAM = (uint8_t*)FCEU_gmalloc(CHRRAMSize);
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
if (!CHRRAM)
return 0;
SetupCartCHRMapping(0, CHRRAM, CHRRAMSize, 1);
AddExState(CHRRAM, CHRRAMSize, 0, "CHRR");
FDSRAMSize = 32768;
core: stdint typedefs, LE optimizations, frame determinism Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in 1185db8. ============================================================== Pass 1: replace custom typedefs with C99 stdint types throughout ============================================================== The custom uint8 / uint16 / uint32 / uint64 / int8 / int16 / int32 / int64 typedefs in src/fceu-types.h were just simple aliases for the C99 stdint.h types. Replace them with the standard names directly. - 498 files modified - ~3,400 token replacements (uint8 -> uint8_t, etc) - fceu-types.h slimmed down to just INLINE / GINLINE / FASTAPASS macros and the readfunc / writefunc function-pointer typedefs (those now use uint8_t / uint32_t natively) - Build clean on `make platform=unix` with zero new warnings - Output binary size unchanged - confirming semantic equivalence Mechanical replacement done with a Python script that uses word- boundary regex to avoid false positives (e.g. 'uint32_t' was correctly left alone because '_' is a word character so 'uint32' is not a complete word inside it). ================================================================ Pass 2: prefer memcpy on LE hosts for endian read/write helpers ================================================================ fceu-endian.c's write32le_mem, FCEU_en32lsb, and FCEU_de32lsb performed bytewise composition/decomposition unconditionally. On LE hosts the in-memory representation already matches the desired LE on-disk format, so a single memcpy is equivalent and lets the compiler emit a single load/store rather than four byte ops. - The bytewise path is kept inside #ifdef MSB_FIRST for BE hosts where it implements the actual byte swap - Both forms produce identical results; this is a code-clarity change more than a performance one (the optimizer was already merging the shifts on LE), but it documents the intent and removes a strict-aliasing-flavoured cast through *(uint32_t*)Bufo - Added missing #include <string.h> in fceu-endian.c which was relying on transitive includes for memcpy Other MSB_FIRST sites in the codebase (state.c FlipByteOrder guards, ppu.c sprite-line rendering, boards/unrom512.c flash-write- counter access) were already optimized for LE; they were verified correct rather than changed. ================================================================ Pass 3: frame determinism for replay and netplay ================================================================ Two libc rand() sites in core were replaced with a local xorshift32 PRNG so that NES games which read uninitialised memory or hit hardware "weak bit" emulation produce reproducible behaviour across runs. NES titles routinely read uninitialised RAM (variables not zeroed before use, sprite Y-position set by junk-on-stack), so the RAM contents at power-on subtly affect game behaviour. With libc rand(), those contents depend on whether anyone else seeded rand() in the same process - a different libretro frontend, a different audio backend init order, or any frontend that does srand(time(0)) all break replay / netplay frame-determinism. 1. fceu.c FCEU_MemoryRand. Used to fill RAM (PowerNES) and CHR-RAM (iNES_Init) at power-on when option_ramstate=2 (random init). Replaced with a local xorshift32 PRNG, exposed via a new FCEU_MemoryRand_Reseed(uint32_t) function called once per power-on: - PowerNES seeds from the first 4 bytes of GameInfo->MD5 (set by all loaders before PowerNES runs) so identical ROMs produce identical RAM, different ROMs differ - iNES_Init seeds from iNESCart.PRGCRC32 before the CHR-RAM fill so two builds of the same ROM get the same CHR-RAM - The PRNG state advances across multiple FCEU_MemoryRand calls within one power-on so RAM and CHR-RAM get different content (matching NES hardware reality) 2. boards/rt-01.c UNLRT01Read. The RT-01 board has 'weak bit' protected EPROM regions; reads of 0xCE80-0xCEFF and 0xFE80- 0xFEFF return 0xF2 with the low 3 bits randomised. Replaced libc rand() with a local xorshift32 seeded at power-on, and added the PRNG state to the savestate via AddExState with key "WBKS" so save / load / rewind / netplay rollback all stay deterministic. In addition, two long-double-to-int truncations were changed to double for cross-platform FP determinism: - sound.c SetSoundVariables: soundtsinc - boards/n106.c DoNamcoSound: inc long double has platform-dependent precision (80-bit on x87, 64-bit with -mfpmath=sse, 128-bit on PowerPC), so the truncated integer result varied across these platforms. double is guaranteed 64-bit IEEE-754 portably. After this pass, the core has no time(), clock(), gettimeofday(), clock_gettime(), getpid(), getuid(), getgid(), getenv(), gethostid(), pthread, std::thread, OpenMP, signal handler, or non-deterministic- malloc dependency. Verified with a Python scanner that greps the source for these patterns; runs clean. The PPU / APU / CPU power-on already explicitly memset all state buffers to 0 (deterministic), and ROM/CHR-ROM allocation already memsets to 0xFF before partial fread (deterministic regardless of file truncation). Combined with the memory-safety hardening in 1185db8 (which prevents savestate-loaded indices from going out-of-bounds and producing unpredictable behaviour), the core now offers genuine frame-deterministic replay across runs, builds, and host endian.
2026-05-04 02:46:34 +02:00
FDSRAM = (uint8_t*)FCEU_gmalloc(FDSRAMSize);
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
if (!FDSRAM)
return 0;
SetupCartPRGMapping(1, FDSRAM, FDSRAMSize, 1);
AddExState(FDSRAM, FDSRAMSize, 0, "FDSR");
SetupCartMirroring(0, 0, 0);
FCEU_printf(" Code : %02x\n", diskdata[0][0xf]);
FCEU_printf(" Manufacturer : %s\n", getManufacturer(diskdata[0][0xf]));
FCEU_printf(" # of Sides : %d\n", TotalSides);
FCEU_printf(" ROM MD5 : 0x%s\n", md5_asciistr(GameInfo->MD5));
2017-10-07 13:35:00 +08:00
FCEUI_SetVidSystem(0);
return 1;
}
void FDSClose(void) {
core: OOM safety, sign-compare cleanup, dead-code removal, const-correctness Omnibus cleanup pass. Build is clean on `make platform=unix` with zero errors and zero warnings, including under `-Wsign-compare -Wstrict-aliasing=2 -Wcast-align`. The `-Wsign-compare` flag is now permanently enabled in WARNING_DEFINES. ================================================================ A. FCEU_gmalloc no longer exit()s on OOM ================================================================ A libretro core must not call exit(): doing so tears down the entire frontend. FCEU_gmalloc previously did exactly that on allocation failure ("Doing a hard exit"). It now returns NULL with the same diagnostic message. Loader-level call sites that can fail their parent function (i.e. return 0 from FDSLoad/iNESLoad/NSFLoad to refuse the cart) now check the return value: - ines.c: trainerpoo, ExtraNTARAM - nsf.c: ExWRAM (both branches) - fds.c: FDSBIOS, CHRRAM, FDSRAM Mapper-level callers (~200 sites) are intentionally left as-is: they live in void-returning Init/Power functions where graceful failure isn't possible without a much larger restructuring. With NULL returns those mappers will null-deref on first access, which is contained to the core - the libretro frontend stays up. This is strictly better than the previous behaviour of exit()ing the entire frontend. ================================================================ B. -Wsign-compare cleanup (27 warnings -> 0) ================================================================ Surveyed every signed/unsigned comparison the compiler flagged and fixed each one. Most fell into a few patterns: Loop variables: changed `int x` to `uint32_t x` for loops over uint32_t counts (TotalSides in fds.c, the trainer-copy loop in 6_8_12_17_561_562.c, soundOffset->SOUNDTS in 594.c). scanline (signed) vs totalscanlines/normal_scanlines (unsigned) in ppu.c: cast (unsigned)scanline at the comparison sites. The scanline values are guaranteed >= 0 at every comparison site (the loop initialises scanline=0 and increments). rate_adjust macro in emu2413.c: the ?: was returning either signed or unsigned depending on `rate`. Cast both branches to uint32_t. Mixed ?: branches in cartram.c (PRGRamSaveSize signed vs WRAMSize unsigned): cast PRGRamSaveSize to uint32_t at use. Other one-off casts in libretro.c, n625092.c, nsf.c, sound.c, zapper.c, eeprom_93Cx6.c. The Makefile change keeps -Wsign-compare permanently enabled so new sign-compare bugs trip CI immediately. Files touched: 594.c, 6_8_12_17_561_562.c, cartram.c, eeprom_93Cx6.c, emu2413.c, n625092.c, fds.c, nsf.c, ppu.c, sound.c, input/zapper.c, drivers/libretro/libretro.c, plus Makefile.libretro. ================================================================ C. NULL-deref hardening on libretro callbacks ================================================================ retro_serialize and retro_unserialize now reject NULL data pointers before passing them to memstream_set_buffer (which would have null-deref'd inside the memstream code). Other retro_* entry points that take pointers were already guarded in earlier passes (retro_set_controller_port_device, retro_get_memory_data, retro_get_memory_size, retro_load_game, retro_cheat_set). ================================================================ D. Mapper coverage spot-check ================================================================ Wrote a heuristic scanner to find the savestate-load-array-index bug class fixed in pass 1 (variable masked at write but unmasked at restore -> OOB index). Scanner flagged 3 candidates across 424 mappers; manual review confirmed all three are false positives: - sachen.c `cmd`: theoretical bug, but only one writer is wired per game and that writer masks at use. - unrom512.c `flash_state` and `latcha`: both already clamped in StateRestore (added in pass 1). The systematic bug class was thoroughly addressed in pass 1. ================================================================ E. Strip never-defined #ifdef symbols ================================================================ Surveyed every #ifdef symbol in the build and cross-checked against #defines (in source and in build files). Removed code gated on symbols that are never defined for any platform target: - DEBUG_MAPPER (datalatch.c, 2 sites): a debug-print NROMWrite handler. Dead. - FRAMESKIP (fceu.h, driver.h, ppu.c, 4 sites): a legacy-FCEU frameskip path. The libretro driver's `skip` argument to FCEUI_Emulate is always 0, and FCEUI_FrameSkip is never called by any libretro frontend. Removed the conditional rendering branch in FCEUPPU_Loop along with the FCEU_PutImageDummy declaration. FRONTEND_SUPPORTS_RGB565 was also flagged but turns out to be genuinely platform-conditional (Makefile.common defines it when WANT_32BPP=0). Kept. ================================================================ F. assert() audit ================================================================ Two assert() calls live in src/ntsc/nes_ntsc_impl.h - third-party NTSC filter code from blargg, both NaN sanity checks (`assert(x == x)`). NDEBUG is in the build flags so they compile to no-ops. No fceumm code uses assert. Nothing to do here. ================================================================ G. const-correctness ================================================================ Function signatures that take strings they don't modify now take const char *: FCEU_printf, FCEU_PrintError (const char *format) FCEUD_PrintError, FCEUD_Message (const char *) FCEU_MakeFName (const char *cd1) md5_update (const uint8_t *input) md5_process (const uint8_t data[64]) also made `static` ================================================================ H. Const-fold static lookup tables ================================================================ Marked static lookup tables const where they are never written: x6502.c: CycTable[256] (cycle counts) md5.c: md5_padding[64] (MD5 padding) vsuni.c: secdata[2][32], secptr (VS security data) palette.c: rtmul/gtmul/btmul[7] (palette multipliers) input/cursor.c: GunSight, FCEUcursor (sprite data) input/pec586kb.c, fkb.c, suborkb.c: matrix (key matrices) boards/8237.c: regperm, adrperm, protarray (mapper perms) boards/datalatch.c: M538Banks (bank table) boards/187.c: prot_data boards/121.c: prot_array boards/bonza.c: sim0reset boards/pec-586.c: bs_tbl, br_tbl boards/178.c: step_size, step_adj (ADPCM tables) boards/244.c: prg_perm, chr_perm boards/bmc42in1r.c: banks boards/emu2413.c: SL (sustain levels) NSFROM in nsf.c looks like a lookup table but is rewritten at runtime to patch in addresses, so it stays mutable. ================================================================ I. Reduce strlen calls ================================================================ Replaced `strlen(STRING_LITERAL)` with `sizeof(STRING_LITERAL) - 1` where the argument is a compile-time-known string literal: - libretro.c retro_set_environment APU loop: was calling strlen("fceumm_apu_") on every loop iteration to compute the same constant offset. - nsf.c visualizer: strlen("Song:"). Other strlen sites in cheat.c, libretro.c, unif.c either already cache to a local size_t or operate on runtime-supplied strings where caching would not help. ================================================================ Build status ================================================================ `make platform=unix` clean: zero errors, zero warnings. With -Wsign-compare -Wstrict-aliasing=2 -Wcast-align: zero warnings. audit_determinism.py: 0 issues. Output binary 4,388,408 bytes (was 4,388,576 from upstream 004c147; -168 bytes from dead-code removal).
2026-05-04 03:55:23 +02:00
uint32_t x;
if (!DiskWritten) return;
for (x = 0; x < TotalSides; x++)
if (diskdatao[x]) {
free(diskdatao[x]);
diskdatao[x] = 0;
}
FreeFDSMemory();
}