2a91d40f4d75ec5affdbd63b95815f14d7a62884
1738 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a91d40f4d |
fds_apu: correct modulator bias values at table indices 3 and 5
The FDS modulator unit's per-step bias table is documented on the
nesdev wiki and used in MDFN, NSFPlay, and negativeExponent's
fceumm_next as { 0, +1, +2, +4, RESET, -4, -2, -1 }. The conditional
form in this file expanded to { 0, +1, +2, +3, RESET, -3, -2, -1 } -
producing the wrong magnitude at indices 3 and 5 where real hardware
takes a power-of-2 step.
Replace the conditional with a literal lookup table matching the
hardware. Bit-identical for mod table values 0, 1, 2, 4, 6, 7; only
indices 3 and 5 change.
This is a step toward #560 (VS Excitebike FDS triangle pitch) but
unlikely to resolve it on its own. The modulator's deeper bug is
that this file is missing the sweep_bias integrator entirely: real
hardware accumulates the bias steps over time and the resulting
running total scales the carrier frequency adjustment, whereas this
implementation applies each mod table entry's bias instantaneously to
the b8shiftreg88 sub-clock gating without accumulation. Tracked
separately; the structural fix needs the modulator unit rebuilt
around an sweep_bias accumulator (or a wholesale backport of
_next's fds_apu rewrite).
|
||
|
|
3f23e2b98f |
sound: fix noise LFSR power-on state to match real hardware (fixes #466)
The internal noise shift register in this file uses a bit-reversed layout vs the canonical nesdev wiki model: output is read from bit 14 (not bit 0), the feedback taps are at 13/14 in long mode and 8/14 in short mode (mirrors of 0/1 and 0/6), and the register shifts left rather than right. The algorithm is algebraically equivalent to the wiki form, but the power-on state has to be mirrored as well. Real hardware initialises the LFSR to $0001 with bit 0 set. Under this file's mirroring, the equivalent state has bit 14 set - i.e. nreg = 0x4000, not nreg = 1. Starting at 1 placed the LFSR 14 steps off-phase from real hardware in long mode and at an entirely different position in the 93-cycle short-mode loop. Reported by NSFPlay author Brad Smith (rainwarrior) comparing moon8.nes audio against Mesen. Reproduced via headless libretro harness with per-channel solo bisection against negativeExponent's fceumm_next branch (which uses the un-mirrored nesdev layout): channel RMS diff vs _next (noise solo, 22-60s window) ------- ---------------------------------------------- square1 1 ( 0.4%) - bit-identical square2 1 ( 0.4%) - bit-identical triangle 178 (14.0%) - minor difference noise 1914 (125.8%) - completely off dmc 2 ( 0.4%) - bit-identical After this patch the noise-solo RMS drops to 6 (0.4%), bringing the noise channel into bit-exact agreement with _next/Mesen/NSFPlay. Full-mix moon8 audio RMS vs _next falls from 1135 to 126 across the 60-second test. |
||
|
|
71bbd10ad0 |
tengen: auto-reload + trigger-on-reach-0 for RAMBO-1 IRQ (#544)
The Tengen 800032 (RAMBO-1) IRQ counter on real hardware auto-reloads from its latch when it would otherwise underflow, and the IRQ triggers when the counter REACHES zero (going from 1 to 0) rather than on the underflow step that follows. Upstream's earlier model triggered on underflow (count 0 -> 0xFF) and then continued decrementing through 0xFE, 0xFD, ... until the game wrote a reload register ($C000/$C001/$E000/$E001 with rmode==1). For titles that don't write the reload register on every IRQ - Skull & Crossbones in 2-player mode is the documented case (libretro docs Compatibility entry), but Hard Drivin', Klax, Rolling Thunder, Road Runner, Xenophobe etc. all use the same mapper - subsequent IRQs after the first underflow are silently swallowed, displacing the status-bar split point and producing the screen-shake / glitch reported in #544. Backport reference: negativeExponent's libretro-fceumm_next commit 7f277a8 ("Cleanup mapper 64/158") plus the Mesen RAMBO-1 model. This patch is the minimal algorithmic delta against upstream: - RAMBO1IRQHook (CPU-cycle mode, IRQmode==1): trigger on IRQCount reaching 0, auto-reload from IRQLatch on the next clock after the trigger. - RAMBO1HBHook (scanline / A12-approximation mode, IRQmode==0): same change. rmode is still set on IRQ fire so existing register-reload semantics on $C000/$C001/$E000/$E001 are preserved bit-for-bit - the writes do exactly what they did before, the only behaviour change is what the IRQ hook itself does between writes. - IRQLatch == 0 edge case handled (reload yields 0, which is the reach-0 trigger condition, so IRQs fire on every clock - the documented hardware behaviour at latch=0). Crucially, this does NOT change: - SFORMAT chunk layout (REGS/CMDR/MIRR/RMOD/IRQM/IRQC/IRQA/IRQL chunks all still present, same byte sizes, same order). Old savestates continue to load correctly. - The write-side dispatch (RAMBO1_Write) for $A000, $8000, $8001, $C000, $C001, $E000, $E001 - all behave exactly as before, including the rmode==1 conditional reload. - Mapper 158 (TENGEN 800037, Alien Syndrome Unl) wiring - it shares the RAMBO1 IRQ hook and gets the same fix for free. - PRG/CHR/mirroring sync logic. - Power-on and StateRestore behaviour. Build verified: tengen.c -O2 / -O3 / aarch64 -O2 clean. |
||
|
|
5f8a864bf3 |
sound: add core option to reduce DMC channel popping (fixes #638)
Games like Castlevania II: Simon's Quest drive the DMC DAC directly
via $4011 writes to produce sample-style audio (separate from the
DPCM bit-stream path). The DAC jumps straight to the newly-written
7-bit value on every write, and those abrupt steps are audible as
clicks/pops when the writes form a low-frequency envelope.
Backport reference: negativeExponent's libretro-fceumm_next applies
a one-step midpoint smoother at the $4011 write site - take the
old DAC value and the new requested value, and store only the
average (rounded toward the new value). Successive writes still
converge on the target, so percussive samples retain their attack;
they just lose half of the per-write step amplitude, which is the
component that produces the click.
Backported as-is, gated on a new core option
`fceumm_reducedmcpopping` (default disabled, so the DAC
trajectory is bit-exact with the original code when the option is
not set). Verified to match _next's algebra exactly across the
boundary cases (full-range jumps in both directions, single-step
deltas, idle no-ops).
The DPCM bit-decode playback path (the +/-2 per bit RawDALatch
update further down in the DMC channel inner loop) is intentionally
NOT touched - that path is the well-behaved bit-stream output, no
popping, and smoothing it would attenuate normal samples.
This addresses the second half of issue #638 (the first being the
triangle ultrasonic gate, already landed as
|
||
|
|
d3cd26e922 |
sound: add core option to mute ultrasonic triangle in HQ (fixes #638)
Castlevania II, Jackal, and several other titles repeatedly drop the
triangle channel into a period range that produces only ultrasonic
output (raw period <= 3, > ~12 kHz at NTSC). The hardware itself
plays these tones, but the DAC reconstruction filter in HQ mode
folds the ultrasonic energy back into the audible range as popping.
The LQ tri/noise/PCM mixer (RDoTriangleNoisePCMLQ) already gates the
triangle channel with `freq[0] <= 4` (i.e. raw period <= 3) for
exactly this reason -- the popping has been absent in LQ since
forever. The HQ path (RDoTriangle) never had the equivalent gate,
so users on Sound Quality = High/Very High hear the popping that LQ
users do not.
Mirror the gate in RDoTriangle, controlled by a new core option
`fceumm_removetrianglenoise` (default disabled). When the option
is off the codegen is identical to before -- gcc folds the
`FSettings.RemoveTriangleNoise && (...)` check via short-circuit
evaluation, the per-sample inner loop is untouched. When on, HQ
behaves like LQ for the triangle ultrasonic case and the popping
disappears.
LQ is intentionally left untouched -- it has always silenced these
tones unconditionally and changing that would be an audible behavior
shift for existing LQ users. The new option is HQ-only.
Backport reference: negativeExponent's libretro-fceumm_next fork
(commit afaa8f5) added the same option, gating both LQ and HQ paths
on it. This patch is the more conservative subset: only adds the
HQ gate, leaves LQ at its current (always-silencing) behaviour.
Build / codegen verified:
- sound.c -O2 / -O3 clean, +15 lines in RDoTriangle (.text),
inner per-sample for-loop unchanged.
- libretro.c clean (options_list[] widened from [25] to [32] to
fit the new key name).
- aarch64 cross-build clean.
|
||
|
|
6911042cf6 |
ppu: rebuild bg pair LUT after priority |= 64, fixes Mega Man 3 sprites
The pair LUT introduced in
|
||
|
|
01b7446a66 |
filter: revert SSE2/NEON FIR kernel — non-bit-exact, audio regressions
The SSE2/NEON FIR added in
|
||
|
|
df8a6e8b26 |
filter: SSE2/NEON inner kernel for NeoFilterSound FIR
NeoFilterSound is the polyphase windowed-sinc downsampler that
converts NES APU output (WaveHi, ~1.79 MHz NTSC) to host sample rate
(44.1/48/96 kHz). The inner loop is a two-output FIR convolution:
for (c = NCOEFFS; c; c--, D++) {
acc += (S[c] * *D) >> 6; // output at integer phase
acc2 += (S[c+1] * *D) >> 6; // output at integer phase + 1
}
NCOEFFS = 484 for soundq <= 1, SQ2NCOEFFS = 1024 for soundq == 2.
Total per frame at 44100 / 60 fps: ~735 outputs * 484 taps * 2 FIRs
= 712k scalar 32-bit MACs, or ~1.4% wall time on a typical
desktop CPU. For NSF playback (no video to render), this is the
single hottest non-emulator-core path.
The coefficient tables (C44100NTSC etc.) and the symmetric mirror
built by MakeFilters store values with max abs ~21588 -- they fit
in int16 losslessly. NES audio sample magnitudes in WaveHi
(channel volumes summed against wlookup1[32] and wlookup2[203])
stay well within int16 for typical content; peaks that would
saturate the int16 pack would also be clipped by the downstream
SexyFilter [-32768, 32767] bound, so saturating the input here is
already part of the audio pipeline's behaviour at full volume.
Refactored the body into a static-inline fir_inner_kernel taking the
sample base, an int16 coefficient pointer, and ncoeffs (NCOEFFS or
SQ2NCOEFFS -- both compile-time constants at the call site, so the
compiler specialises the kernel for each branch). Adds int16 mirror
tables coeffs16[] and sq2coeffs16[] built once per filter rebuild in
MakeFilters.
The SSE2 path (`#if defined(__SSE2__)`) processes 8 taps per
iteration with pmaddwd: each instruction does 4 lanes of 2-tap
multiply-add against int16 operands, returning 4 int32 partial sums.
Packs samples int32 -> int16 with PACKSSDW saturation; loads coeffs
already-int16 from coeffs16[]. The NEON path
(`#if defined(__ARM_NEON)`) follows the same shape with vqmovn_s32
for the pack and vmlal_s16 for the multiply-accumulate, splitting
each 8-lane fold into a pair of 4-lane vmlals. Both paths apply the
`>> 6` once at the end rather than per-term as the scalar does --
sub-LSB drift at the final 16-bit output (measured 0.014% on a
synthetic 484-tap reduction, well below audible threshold and below
the existing SexyFilter `(t - sexyfilter_acc1) >> 16` rounding).
Tail handles ncoeffs % 8 (4 taps for NCOEFFS=484, 0 for
SQ2NCOEFFS=1024) and is the entire window on builds without SSE2 or
NEON, preserving the original scalar behaviour exactly there.
Build verified: x86_64 -O3/-O2 with SSE2 emits pmaddwd / packssdw /
paddd in the inner loop; x86_64 -mno-sse2 falls back to pure scalar;
aarch64-linux-gnu cross-build emits smlal / sqxtn in the inner loop.
Microbench (484-tap FIR, 30k iters * 735 outs * 2 FIRs, gcc 13.3):
scalar: 7196 ms ( 3.0 GMAC/s)
SSE2 int16: 1764 ms (12.1 GMAC/s) 4.09x at -O3
SSE2 int16: 1761 ms (12.1 GMAC/s) 4.06x at -O2
Result magnitude vs scalar: +0.014 % (sub-LSB on the int16 audio
output). The SQ2NCOEFFS=1024 path scales the same 4x: at 1024 taps
the absolute win is roughly twice as large in cycle terms.
|
||
|
|
65f076de8f |
fceumm: fix -Wsign-compare in FCEU_fseek
offset is long while EMUFILE size/location are uint32_t, so the SEEK_SET and SEEK_CUR bounds checks mixed signed and unsigned operands. Cast to uint32_t for the comparisons, but first reject the cases a bare cast would silently wrap: a negative SEEK_SET offset, and a SEEK_CUR rewind past the start. Behaviour for valid seeks is unchanged; invalid negative seeks now fail cleanly instead of wrapping to a huge offset. |
||
|
|
e4b039b604 |
Fix MSVC C89 build: portable unaligned 64-bit store in pputile.h
The per-tile fast path used a GCC-only typedef typedef uint64_t fceu_u64_unaligned __attribute__((may_alias, aligned(1))); which MSVC cannot parse, producing C2146/C2065/C2143 at every point pputile.h is textually included in ppu.c. Split the store by compiler: keep the may_alias typedef for GCC/Clang (preserving the direct movq) and use memcpy on MSVC, which is strict-aliasing-safe and lowers to the same unaligned 64-bit store. ppu.c already includes <string.h> and is the only includer, so memcpy is always in scope. The packed value stays declared at the top of the block, keeping the C89 declaration-before-statement rule intact. |
||
|
|
a502ceafc4 |
ppu: precomputed pair LUT for the per-tile palette gather
The pputile.h `if (X1 >= 2)` block does the 8-pixel per-tile pen
expansion via eight serial nibble loads:
P[0] = PALRAM[pixdata & 0xF]; pixdata >>= 4;
P[1] = PALRAM[pixdata & 0xF]; pixdata >>= 4;
... [8 iterations]
gcc -O3 already does an impressive job: it unrolls the eight shifts
into independent loads from the L1-resident 16-byte PALRAM, then
folds the eight bytes into a single 64-bit register via a
shift-OR-accumulate chain and emits one movq to P. Even so, the
per-tile gather shows up as the dominant (a)-case missed-vec site
in -fopt-info-vec-missed for RefreshLine (10 hits at ppu.c:367 and
4-hit clusters at every pputile.h include site), "control flow in
loop" being the headline reason.
Replacing it with a precomputed 256-entry pair LUT halves the load
count: each 8-bit slice of pixdata indexes pair_lut[256] returning
a 16-bit value with two expanded pens, so four 16-bit loads and a
shift-OR-accumulate of four halves produce the same 64-bit tile
word that one movq writes to P. The LUT is rebuilt at the start of
every RefreshLine call (~768 ops per rebuild, ~240 rebuilds/frame
plus the occasional mid-scanline FCEUPPU_LineUpdate call), so
mid-frame raster effects writing to $3F00-$3F0F are reflected on
the very next scanline.
A subtlety found while implementing: the natural
uint64_t packed = ...; memcpy(P, &packed, 8);
(or a packed-struct equivalent) routes the store through a stack
scratch -- gcc spills `packed` to %rsp, then "copies" 8 bytes back
out via a memcpy lowering that re-reads the same stack slot. Result
is *slower* than the original eight-load scalar. Using a
`uint64_t __attribute__((may_alias, aligned(1)))` typedef sidesteps
that path entirely; gcc emits a direct `movq %reg, -8(%r13)` and
the optimisation lands.
Codegen verified: 12 distinct pputile.h expansion sites in the .s
each emit four `movzwl 0(%rbx,...,2)` LUT loads + a shift-OR
accumulate + one `movq %X, -8(%r13)` to P, with no %rsp traffic
in the inner loops. gcc hoists the LUT base into %rbx once at
function entry and reuses it across all 12 sites.
Microbench (32 tiles x 240 scanlines x 1000 frames; PALRAM and
pixdata refreshed each iter to defeat caching games, LUT rebuild
charged on every iteration to the new path):
-O3: 22.4 ms -> 9.9 ms (2.25x) 2730 -> 6200 Mpix/s
-O2: 27.1 ms -> 12.3 ms (2.21x) 2267 -> 5005 Mpix/s
Bit-exact across 2000 random PALRAM+pixdata permutations.
|
||
|
|
3a84a6fd0b | Cleanup endian code | ||
|
|
a3467f6af9 |
core: savestate hardening - reject malformed states, bound APU table indices
Pass 10 audit of the savestate path against malformed input under
AddressSanitizer + UndefinedBehaviorSanitizer. retro_unserialize is
attacker-controlled in any frontend that loads .state files from disk,
so any out-of-bounds read/write or DoS in the parser is a real attack
surface.
Real OOB-read crash caught by ASAN savestate fuzzing
====================================================
Random-byte mutation of a valid savestate hits a SEGV in
RDoTriangleNoisePCMLQ at sound.c. The LQ Tri/Noise/PCM mixer indexes
wlookup2[scaled_tcout + noiseout + scaled_dmc]
with all three operands derived from state fields that are loaded
straight from the savestate (lq_tcout as raw uint32_t,
EnvUnits[2].decvolume as uint8_t, RawDALatch as uint8_t). The static
table is only 203 entries; under normal play the channel-write paths
keep the operands inside [0, 202] (lq_tcout ≤ 45, noiseout ≤ 30,
RawDALatch ≤ 127), but a malformed state can drive the index to any
value, producing a heap-buffer-overflow read in the audio inner loop.
Same class of bug at four more wlookup2 sites in the same function,
plus the HQ wlookup2[(b >> 16) & 255] in WriteHi (mask is 8 bits, but
the table is only 203 entries so the upper 53 indices are OOB), and
wlookup1[b >> 24] (mask 8 bits, table only 32 entries).
A separate but related issue at the LQ Squares mixer: wlookup1 is
indexed by ttable[0][RectDutyCount[0]] + ttable[1][RectDutyCount[1]],
where RectDutyCount[] is loaded from savestate as int32_t and may be
> 7. ttable is int[2][8], so the indirection itself reads adjacent
memory before feeding it into wlookup1.
Crashes also caught by targeted-input testing on master: an all-zero
state, an all-0xFF state, or any state with a corrupted magic header
all crash because the chunk loop enters with totalsize parsed as
INT_MIN and never exits cleanly.
Changes
=======
src/state.c:
* ReadStateChunk: 4-byte 'toa' read now requires exactly 4 bytes
returned (was <= 0, allowing 1- to 3-byte short reads to leave
toa[] partially uninitialised before strncmp consumed it). Same
for the per-chunk payload memstream_read into tmp->v. A failed
memstream_seek on an unknown chunk is now a parse error - the
loop previously kept spinning at the same offset on out-of-range
seeks because the return was ignored.
* ReadStateChunks: chunk size + 5 housekeeping is now overflow-
checked before being subtracted from totalsize. The previous
'totalsize -= size + 5' computed size + 5 as uint32_t, so a
chunk size of e.g. 0xFFFFFFFB wrapped to 0 and the outer loop
failed to make progress.
* FCEUSS_Load_Mem: zero-initialise the 16-byte stack header so
short reads see deterministic zeros. memstream_read return now
checked. NULL-check memstream_open. Read totalsize as uint32_t
and reject any value > INT32_MAX rather than silently casting
to a negative signed int that makes the chunk loop never enter.
src/sound.c:
* New helper wl2(idx) clamps the index to wlookup2[]'s size before
reading. Used at all five LQ Tri/Noise/PCM call sites and the HQ
WriteHi pass.
* HQ wlookup1[b >> 24] in WriteHi now masks index by & 31 to match
the 32-entry table.
* LQ Squares mixer: RectDutyCount[x] masked & 7 at every
ttable[x][RectDutyCount[x]] call site. The inner-loop increments
already mask the value but the first call after savestate load
did not.
Verification
============
Random-byte fuzz: 50,000 iterations of a baseline savestate with 1-32
random byte mutations each, run through retro_unserialize +
retro_run. Master crashes within ~50 iterations (sound.c SEGV in
RDoTriangleNoisePCMLQ); pass 10 completes 50,000 / 50,000 iterations
without any ASAN-reported issue.
Targeted edge cases (wrong magic, totalsize overflow, chunk-size
+ 5 wrap, all-zero state, all-0xFF state): every input that
crashed master now rejects cleanly on pass 10.
Bit-exact regression: master vs pass 10 on 9 valid test ROMs (silent,
idle, active, 5-channel max-volume stress, plus the per-APU-channel
ROMs from the earlier APU pass) at 1500 audio frames each - byte-
identical audio output. Video output identical at 60 frames each.
Cross-version compatibility: states saved on master load on pass 10
and vice versa; no format change.
Pre-existing issue noted but not fixed in this pass: retro_unserialize
unconditionally returns true if the size matches, regardless of
whether FCEUSS_Load_Mem actually accepted the state. The frontend has
no way to know a malformed savestate was rejected. Worth a follow-up
to plumb a return code through FCEUSS_Load_Mem to retro_unserialize.
|
||
|
|
7939d92b3e |
core: ROM loader hardening - reject malformed iNES/UNIF/FDS inputs
Pass 9 audit of the iNES, UNIF, and FDS loaders against malformed ROM
input under AddressSanitizer + UndefinedBehaviorSanitizer. The ROM file
is the entry point for untrusted input, so any out-of-bounds read,
write, integer overflow, or DoS in the parsers is a real attack
surface for anyone who downloads ROMs from third-party sites.
Two pre-existing crash inputs caught by ASAN
============================================
test_zero_prg.nes : iNES 2.0 header that decodes to PRGRomSize == 0.
Master loads it: uppow2(0) = 0, FCEU_malloc(0)
returns an implementation-defined small/null
buffer, and the first PRG fetch from the reset
vector reads past the heap.
=> heap-buffer-overflow at cart.c:138 CartBR
test_mapr_huge_unif.unf : UNIF MAPR chunk with declared size 0xFFFFFFFF.
Master computes FCEU_malloc(uchead.info + 1)
which overflows uint32_t to FCEU_malloc(0); the
subsequent fread of 4 GiB then heap-corrupts and
boardname[uchead.info] = 0 finally segfaults.
=> SEGV at unif.c:345 SetBoardName
Plus a UNIF CTRL DoS: a CTRL chunk declaring 4 GiB drives the
diagnostic dump loop through 4 billion FCEU_fgetc() calls (and the
loop didn't even break on EOF), printf'ing each byte to the log.
Pass 9 fuzz inputs reject every one of those cleanly. Bit-exact
identical audio + video output to upstream master across nine valid
test ROMs (silent, idle, active gameplay, 5-channel max-volume stress,
and the per-APU-channel test ROMs).
Changes
=======
src/ines.c (iNESLoad):
* Reject PRGRomSize <= 0. The legacy iNES path already substitutes
ROM_size = 256 when the byte is zero, but the iNES 2.0 exponent
encoding can still resolve to zero from a malformed header.
* Cast PRGRomSize + CHRRomSize to uint64_t before adding. Both fields
are int with a 0x40000000 cap from iNES_read_header_info, so the
addition can reach 0x80000000 - signed overflow (UB) that wraps
negative and then sign-extends bogusly into the uint64_t romSize
used for size sanity prints.
* Check the FCEU_fread return value on every read (trainer, PRG,
CHR, MiscROM) and warn on truncation. The buffers are pre-filled
with 0xFF (open bus) or zero, so a short read leaves a sane tail,
but the user got no warning that the file was incomplete.
src/unif.c:
* FixRomSize: change return type from int to uint32_t. The 0x80000000
cap was being implicitly converted from signed int (where it is
INT_MIN) to the uint64_t UNIF_PRGROMSize/CHRROMSize callers, sign-
extending to 0xFFFFFFFF80000000 - a 16 EiB malloc request.
* SetBoardName: cap uchead.info at 256 (in addition to the existing
< 4 reject). Real UNIF board-name strings are < 32 characters.
Without the upper bound, uchead.info near 0xFFFFFFFF makes
(uchead.info + 1) wrap to zero and FCEU_malloc returns a tiny
buffer that the subsequent 4 GiB fread heap-overflows. Also check
the fread return.
* DoMirroring / CTRL: cap the diagnostic dump-and-skip path at 16
bytes and FCEU_fseek past the remainder. Without the cap, a chunk
declaring info ~ 0xFFFFFFFF would spin 4 billion FCEU_fgetc/printf
iterations - CPU-bound DoS. The CTRL branch also lacked an EOF
break, so a truncated stream would keep returning -1 forever.
src/fds.c (SubLoad):
* Zero-initialise the stack header[16] so partial reads see
deterministic zeros rather than stack garbage. The fsize >= 16
guard reduces the chance of a short read but doesn't eliminate
it (mid-stream IO error etc).
* Check the FCEU_fread return on the header read and on each
65500-byte side read; warn on truncation.
Verification
============
ASAN/UBSAN sweep across 18 fuzz inputs:
* Every case that crashed master now LOAD_REJECTED on pass 9.
* No new UBSAN/ASAN findings introduced (the pre-existing
ppu.c:85 left-shift UB shows up identically on both sides).
Bit-exact audio dump regression vs master across 9 valid ROMs at 600
frames each: identical output. No behavioral change for legitimate
input.
|
||
|
|
6c97c3fff4 |
core: APU channel disable - fix bitmasks and LQ-mode volume application
Two related bugs in the user-facing fceumm_apu_1..5 channel disable
toggles. Pre-existing since commit
|
||
|
|
6ffed08948 |
core: video pipeline - SW framebuffer, hoist invariants, drop NTSC memcpy
Rework retro_run_blit to: * Try GET_CURRENT_SOFTWARE_FRAMEBUFFER for zero-copy. If the frontend hands us its own scanout buffer with a compatible pixel format, the blit writes the palette-LUT conversion directly into it instead of routing through fceu_video_out. Falls back to the existing buffer when the frontend declines or returns a mismatched format. The pixel format negotiated at retro_load_game is now cached in a file- scope active_pixformat for this validation. * Drop the per-frame memcpy in the NTSC path. nes_ntsc_blit writes into ntsc_video_out with a wider stride (NES_NTSC_WIDTH includes right-margin padding); the previous code copied row-by-row into a tightly packed fceu_video_out before video_cb. The pitch parameter to video_cb already lets the frontend skip past padding per scanline, so the copy was redundant. Saves ~580 KB / frame at 32 bpp - around 35 MB/s of memory traffic at 60 fps. * Hoist loop-invariant decisions out of the per-pixel inner loop. GameInfo->type != GIT_NSF and use_raw_palette do not change mid- frame; emphasis (XDBuf) is uniform per scanline because the PPU writes one colour_emphasis byte to all 256 entries of dtarget at ppu.c:683-685. The deemphasis branch is now once per row instead of once per pixel. * Replace fceu_video_out[y * width + x] indexing with a running out_row pointer (strength reduction). * Remove the unused incr local. Bit-exact verified against the baseline across 4 test ROMs (silent, idle, active gameplay, all-channel max-volume stress) for non-NTSC, SW-framebuffer-granted, and NTSC composite paths - 1200+ frames total. Audio output also bit-identical. Determinism audit pass over the video pipeline turned up no issues: no rand/srand/time/clock/gettimeofday in any core path; PPU writes to XBuf are scanline-deterministic; nes_ntsc_blit is stateless; burst_phase initializes to zero and toggles deterministically; and the existing FCEU_MemoryRand and rt-01 weakbits state machines from earlier passes remain in place. |
||
|
|
b5b43ad2be |
core: revert LQ dispatch deduplication from pass 6 (audio regression)
Pass 6 (commit
|
||
|
|
9f6b84cf33 |
core: audio determinism fix and pipeline cleanup
Audit pass 6 - audio path. The libretro core only ever outputs 16-bit
stereo, so the question was whether anything in the audio pipeline is
either non-deterministic or pointlessly busy given that constraint.
DETERMINISM BUG FIX
src/filter.c's `sexyfilter_acc1` and `sexyfilter_acc2` are file-scope
int64_t IIR accumulators. They're saved/restored with the rest of the
sound state (sound.c "FAC1"/"FAC2" SFORMAT entries), but they were
NOT reset by FCEUSND_Power on cart load. So a second cart loaded in
the same process inherited the first cart's IIR state - audibly
minor (a few samples of transient before the IIR re-converges) but
non-deterministic across load orders.
`SexyFilter2`'s lowpass accumulator was even worse: a function-local
`static int64_t acc = 0` that no other code could touch. Its first-
load value was 0; from then on it just accumulated forever, with no
way to reset it. Same load-order-dependence problem, plus it wasn't
even savestate'd.
Fix:
- Lift SexyFilter2's local static to file scope as `sexyfilter2_acc`.
- Add `void SexyFilter_Reset(void)` that zeros all three accumulators.
- Call SexyFilter_Reset() from FCEUSND_Power.
- Add `sexyfilter2_acc` to the SFORMAT savestate list as "FAC3" so
runahead/replay/netplay restore it alongside FAC1/FAC2.
`mrindex` (filter.c file-scope, NeoFilterSound's input cursor) is
already reset in MakeFilters, which FCEUI_Sound calls on every cart
load, so it doesn't need explicit handling. `mrratio` is set in the
same path. Documented in the new SexyFilter_Reset comment.
PIPELINE CLEANUPS
(1) WaveHi memset bound. The HQ-mode flush was clearing the entire
WaveHi[40000] (160 KB) past `left` every frame, but channels only
write into [left, SOUNDTS) - everything past SOUNDTS is already
zero from the previous frame's clear (or FCEUSND_Power on the
first frame). Tightened the memset to (SOUNDTS - left) * 4 bytes
instead of sizeof(WaveHi) - left * 4. SOUNDTS is bounded by NES
cycles per frame (~30000 NTSC), so this saves ~40 KB of
pointless memset every HQ frame. The (SOUNDTS > left) guard
handles the degenerate case of a very short frame where SOUNDTS
might not have advanced past the coefficient history.
(2) RDoSQLQ silent-channel branch. When both square channels are
inactive, `amp[x]` is forced to 0 (line 634), which propagates
through `ttable[x]` to make `totalout = wlookup1[0] = 0`. The
previous code looped (end - start) iterations adding 0 to
Wave[V>>4] - genuinely no-op for ~30000 NES cycles per frame
when nothing is playing. Removed the loop body.
(3) LQ dispatch deduplication. `RDoSQLQ` mixes both squares in a
single call; `RDoTriangleNoisePCMLQ` mixes triangle/noise/PCM
in a single call. The previous code pointed DoSQ1 and DoSQ2
both at RDoSQLQ, and DoTriangle/DoNoise/DoPCM all at
RDoTriangleNoisePCMLQ. The first call did the work; the
subsequent calls entered the function only to be rejected by
the `if (end <= start) return;` guard at the top. Now point
DoSQ1/DoTriangle at the real workers and DoSQ2/DoNoise/DoPCM
at Dummyfunc - same behaviour, no redundant call+early-return
on every frame.
(4) stereo_filter_apply_delay copy loop -> memcpy. The element-by-
element copy `samples[i+pos] = sound_buffer[i]` is trivially
equivalent to memcpy for non-overlapping int32_t blocks; memcpy
lets the compiler/libc dispatch SIMD where available.
(5) Dead code in filter.c. Removed the `#ifdef moo` and `#if 0`
blocks (legacy commented-out lowpass formula attempts that
have been dead since the original blargg import) and the
`<math.h>` include, which is no longer needed once those
blocks are gone.
ARCHITECTURAL NOTES (not changed - documented for future passes)
- Per-frame sample count varies by +/-1 (e.g. 798/799 at 48000Hz
60Hz NTSC). This is required by libretro's variable-batch audio
model and is fundamental to the deterministic phase accumulator
(`soundtsoffs` carries the fractional remainder across frames).
Same input -> same count sequence. Replay/netplay-stable.
- The `stereo_filter_apply_null` post-pass exists to convert
in-place from int32 mono samples to int32 stereo-packed
((s<<16)|(s&0xFFFF)) format that, when the buffer is cast to
int16_t*, reads correctly as L,R,L,R pairs. Could be folded
into SexyFilter's clamp step to save one full-buffer pass
(~190 KB/s of redundant memory traffic) but requires sound.c
to know about the libretro frontend's stereo filter selection.
Too much frontend/core coupling to chase for a modest cache
win; left as-is.
- HQ path runs 5 in-place passes through WaveFinal[] per frame
(NeoFilterSound, SexyFilter, optional SexyFilter2,
stereo_filter_apply, audio_batch_cb read). LQ path runs 4.
Each pass is ~3.2 KB at 48000Hz - ~1 MB/s of buffer traffic
total. Modest; not worth restructuring.
- WaveHi[40000] is BSS-allocated (160 KB) even when in LQ mode
where it's never written. Cost is zero - BSS pages don't
materialise until first write - so making it conditional has
no runtime benefit, only adds complexity.
- stereo_filter_delay uses a linear buffer with memmove on each
frame to slide consumed samples back to position 0. A circular
buffer would eliminate the memmove but only matters when the
user opts into the delay filter (off by default).
Build clean under -std=gnu11 with -Wno-write-strings -Wsign-compare
-Wundef -Wmissing-prototypes; zero errors, zero warnings.
audit_determinism.py: no rand/time/long double/threads issues.
|
||
|
|
6359a0b36e |
core: stride-aware savestate, iNES2 helpers, -Wundef, -Wmissing-prototypes
Audit pass 5 - five distinct cleanups bundled into one omnibus.
1. Element-stride byte-swapping for savestate fields (state.h, state.c,
fceu-endian.{h,c})
The SFORMAT 's' field was previously {bit 31 = RLSB, bits 0..30 =
byte size}. RLSB triggers FlipByteOrder() on MSB hosts, which
reverses the entire entry buffer end-to-end. That is correct for a
single primitive (size <= 8 bytes) but wrong for an array of
multi-byte primitives - reversing the whole buffer would swap
element 0 with element N-1 and reverse their bytes too, scrambling
the data.
The previous workaround was either splitting an N-element array
into N separate single-primitive entries with distinct chunk IDs
(n106 PlayIndex split into IDX0..IDX7) or skipping the entry
entirely on big-endian hosts (the GEKKO #ifndef in vrc6.c / vrc7.c).
Both approaches mean BE saves are not portable to LE and vice
versa, and force the same workaround at every new array site.
This pass adds proper stride support:
* SFORMAT 's' encoding is now {bit 31 = RLSB, bits 24..30 =
stride in bytes (0 = legacy/unset), bits 0..23 = byte size}.
16 MiB max size, well above any actual savestate field.
* FCEUSTATE_RLSB_ARRAY(stride) macro for the new pattern.
* FlipByteOrderStrided() byte-swaps each element of an array
independently. Round-trip identity verified: [01 00 00 00 ...]
-> [00 00 00 01 ...] -> [01 00 00 00 ...].
* state.c's SubWrite / ReadStateChunk / CheckS use new helpers
sf_size() / sf_stride() / sf_flip() that mask the size out of
the new bit layout and dispatch to the strided variant when
stride < size.
Backwards compatible: legacy single-primitive entries (size == 1,
2, 4, 8) leave the stride bits at zero, which sf_stride() reads
as "stride equals size" and falls through to FlipByteOrder() as
before. No on-disk format change. Existing single-primitive RLSB
sites are unchanged.
The infrastructure is now in place so any future SFORMAT entry
that is an array of multi-byte primitives can be expressed as a
single entry (e.g. "{ buf, sizeof(buf) | FCEUSTATE_RLSB_ARRAY(4),
"BUF." }") without splitting or skipping. The existing PlayIndex
split and GEKKO #ifndefs are intentionally left untouched -
migrating them would alter the on-disk savestate format and is a
separate decision.
2. iNES1-vs-iNES2 sizing helpers (cart.h)
Twelve sites across the codebase encoded the same conditional:
info->iNES2 ? (info->PRGRamSize + info->PRGRamSaveSize) : default
Sometimes for PRGRAM, sometimes for CHRRAM, sometimes in bytes,
sometimes after dividing by 1024. The pattern is verbose and easy
to write inconsistently.
Added two inline helpers in cart.h:
- CartInfo_PRGRAM_bytes(info, default_bytes)
- CartInfo_CHRRAM_bytes(info, default_bytes)
Migrated 9 of the 13 sites: cartram.c (2), 162.c, 163.c, 134.c,
399.c, 478.c, 480.c, 484.c. The remaining 4 are non-helper-fitting
variants (164.c special masking, 2 cartram SaveGameLen sites with
different fallback semantics, mmc3.c Boogerman submapper detection).
3. -Wundef enabled permanently (Makefile.libretro)
Zero warnings out of the box - no #if-on-undefined-macro footguns
in the codebase. Now part of WARNING_DEFINES alongside the existing
-Wsign-compare.
4. -Wmissing-prototypes enabled permanently (Makefile.libretro)
Started at 198 warnings, cleared all of them:
* Mass-static-ified ~96 functions across 75 files that were
defined non-static but only used within their own translation
unit. (See static_prototype_fixer.py in the development notes.)
* K&R-style empty-parens prototypes "()" replaced with explicit
"(void)" across all asic_*.{c,h} files - GCC treats "()" as
"any args" and refuses to match it against a separate K&R
definition.
* Added missing forward declarations to public headers:
- fds.h (FDSLoad)
- nsf.h (NSFLoad)
- ines.h (iNESLoad)
- unif.h (UNIFLoad)
- latch.h (LatchHardReset, K&R fix)
- eeprom_93Cx6.h (eeprom_93Cx6_read, K&R fix)
Each header gained an "#include "file.h"" where needed.
* fds_apu.c now includes its own fds_apu.h header (was missing).
* fds_apu.h: removed unused FDSSoundRead declaration (the function
is internal-static).
* cartram.h: removed unused CartRAM_close declaration (the function
is internal-static).
* input.h: added a centralised block of FCEU_Init* prototypes
(Zapper, Mouse, Powerpad, Arkanoid, VirtualBoy, FKB, SuborKB,
PEC586KB, HS, Mahjong, FamilyTrainerA/B, OekaKids, TopRider,
BarcodeWorld, BattleBox, QuizKing, FTrainerA/B, SpaceShadow,
LCDCompZapper, ArkanoidFC) plus FCEU_ZapperSetTolerance. These
were previously declared as "extern" inside src/input.c.
* Static-ified FP_FASTAPASS callbacks in 106.c, 65.c, 67.c,
asic_h3001.c, asic_vrc3.c (those with no external callers);
left non-static for those that have header decls or are
referenced from sibling .c files (asic_mmc1, asic_vrc6,
asic_vrc7, flashrom).
* For a small set of cross-file functions where adding a header
was disproportionate to the value (MMC5_hb, NSFMMC5_Close,
GetKeyboard, FCEU_GetJoyJoy), placed a forward declaration
immediately above the definition. This satisfies
-Wmissing-prototypes (which checks for any prior declaration
in scope) without churning the public-header layout.
5. -Wshadow partial cleanup (not enabled permanently)
Fixed five real shadows that were either bugs or actively
misleading:
* src/boards/476.c: removed an inner "int i" that shadowed the
outer loop counter.
* src/boards/mmc5.c MMC5_hb: parameter "scanline" renamed to
"sl_param" (was shadowing the global "scanline").
* src/boards/n106.c DoNamcoSound: parameter "Wave" renamed to
"WaveBuf" (was shadowing the global Wave audio buffer); also
updated the forward declaration and the matching parameter
on sound.h's NeoFill function pointer typedef.
* src/boards/vrc7.c UpdateOPLNEO: same Wave -> WaveBuf rename.
* src/ntsc/nes_ntsc_impl.h: renamed an inner loop counter "n"
that shadowed an outer "n".
* src/drivers/libretro/libretro.c FCEUD_RegionOverride: local
"pal" renamed to "is_pal" (was shadowing the typedef "pal" from
palette.h).
* src/palette.c FCEUI_SetPaletteArray: parameter "pal" renamed
to "data" (same shadow); driver.h declaration updated to match.
-Wshadow itself is NOT enabled permanently because the remaining
warnings are deliberate parameter naming conventions (XBuf in
draw functions, X in cpu hooks) and third-party blargg ntsc code.
In addition, four files were touched as part of an MSVC-build fix
that came up mid-pass: src/fds.c, src/nsf.c, src/ines.c, and
src/drivers/libretro/libretro_dipswitch.c had snprintf() calls
introduced in pass 4 that fail to link on pre-MSVC2015 toolchains
when STATIC_LINKING=1 (the libretro-common compat_snprintf.c shim
isn't compiled in those configurations). Replaced each snprintf with
either sprintf-into-bounded-buffer (the format strings have known
maximum output) or strlcpy/strlcat for the dipswitch key-build case.
All output is still bounded; truncation happens via strl*'s normal
truncation semantics where applicable.
All added code is C89-clean (top-of-block declarations only, no
mixed decls, no // comments, INLINE macro from fceu-types.h instead
of bare "inline"). Builds clean under -std=gnu11 with -Wno-write-
strings -Wsign-compare -Wundef -Wmissing-prototypes; zero errors,
zero warnings.
Determinism audit (audit_determinism.py): no rand/time/long
double/threads issues introduced.
|
||
|
|
492b335705 |
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).
|
||
|
|
004c147d32 |
core: strict-aliasing fixes, bounded string ops, drop FCEUDEF_DEBUGGER
Three follow-up cleanups on top of the determinism / typedef / LE work in |
||
|
|
f2a5be381d | Fix warning on Android | ||
|
|
766f84662b |
core: stdint typedefs, LE optimizations, frame determinism
Three follow-up audit passes on top of the memory-safety / leak / savestate-portability work in |
||
|
|
1185db89c1 |
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.
|
||
|
|
448a231618 |
Merge pull request #644 from Swordfish90/feature/android-add-16kb-pages-support
Add support for 16KB pages on Android. |
||
|
|
72923b261d | Add support for 16KB pages on Android. | ||
|
|
718572bfc2 |
Merge pull request #641 from cscd98/webos-ci
libretro: add webOS to CI |
||
|
|
1722c801fa |
Merge pull request #643 from keithbowes/fix-linker
Fixed the "multiple definitions of submapper" linker error |
||
|
|
c1abf68d22 | Fixed the "multiple definitions of submapper" linker error | ||
|
|
140630470c | Merge pull request #642 from NRS-NewRisingSun/educational | ||
|
|
147eb59b6b | Add mappers 341.1 and 343. | ||
|
|
37e23ac075 | Cleanup | ||
|
|
13f16f05ca | Mappers 261: Reset to menu from Galaxian | ||
|
|
6657dc502f | Mappers 344: Add solder pad | ||
|
|
ba25f9107b | Mappers 83/264: Added submapper 3, rewrote to incorporate new findings | ||
|
|
d5720a9072 | libretro: add webOS to CI | ||
|
|
e81b5e0370 | Add mapper 487 submapper 1. | ||
|
|
d20e1cf637 | Add mapper 617. | ||
|
|
93300cbf07 | Add mapper 618. | ||
|
|
ee613aec52 | Mapper 607: Correct for multiple UNROM games. | ||
|
|
bcd85e6cf3 | Mapper 137: Correct mirroring for The Great Wall. | ||
|
|
dbf1f2eb83 |
Merge pull request #640 from warmenhoven/warmenhoven/pr/linux-aarch64
add linux-aarch64 build |
||
|
|
6eabcd2b89 |
add linux-aarch64 build
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
bf2a9ad3d5 | Add mapper 255 as a variant, rather than a duplicate, of mapper 225. | ||
|
|
b60fc16d15 | Add mapper 49.1, resembling the UNIF MAPR BMC-STREETFIGTER-GAME4IN1. | ||
|
|
d180d7c7ad | Mapper 439: Add solder pad. | ||
|
|
1d9c0d739a | Mapper 437: Use value instead of address. | ||
|
|
a5df7359bc | Add mapper 615. | ||
|
|
58007df8bc | Add mapper 614. | ||
|
|
eb6df7680a | Add mapper 612. |