The illuminance-sensor code in input/common/linux_common.c used raw
pthreads: pthread_setcancelstate() around the file-reading critical
section, and pthread_cancel() on close via a pthread_t reconstructed by
casting sthread_get_thread_id() -- a fragile round-trip through the
opaque id.
Add cancellation primitives to rthreads and route the driver through
them:
void sthread_set_cancel_enable(bool enable);
bool sthread_cancel(sthread_t *thread);
sthread_cancel() takes the sthread_t directly and uses its backing handle,
dropping the pthread_t cast. Both are implemented with
pthread_setcancelstate()/pthread_cancel() on pthread backends that provide
them, gated by RTHREADS_HAVE_CANCEL which excludes Win32, GEKKO, 3DS and
Android/Bionic (Bionic has no pthread_cancel; rthreads.c is compiled there
even though linux_common.c is not). Where unsupported they are a no-op /
return false, and the existing cooperative sensor->done flag still stops
the thread.
linux_common.c no longer includes <pthread.h> or references pthread
directly. No functional change on Linux.
The test input driver's keyboard matrix - eleven kilobytes across
pads and keycodes - was the last static of the test-driver family
still resident in every default binary. It now allocates alongside
the step array when the driver starts and frees on teardown, with
the keyboard clear running before the free and guarding an absent
matrix.
The test joypad driver, test input driver and built-in net-retropad
core each held a static two-hundred-step array - 54, 54 and 411
kilobytes - and the test drivers ship by default, so every binary
carried 520 kilobytes of test scaffolding in bss. On platforms with
demand paging untouched bss is free; on the consoles without it -
exactly where memory matters - bss is cleared at load and fully
resident. The arrays are now allocated when the driver or core
actually initializes and freed on teardown, taking the binary's bss
from 1620 to 1100 kilobytes. Both validation dumps byte-identical.
The same census flags the cloud-sync state at 297 kilobytes and the
Discord queue at 197 as the next candidates of this class.
CoreHaptics signals some failures by throwing an NSException
(Haptic_RaiseException, HapticUtils.h) instead of populating the
NSError out-param. This happens when a pattern player's start/
sendParameters/stop is invoked - or when the lazy strongPlayer/
weakPlayer getters (re)create and start the engine - while the
engine is not running. iOS auto-stops the engine on backgrounding,
audio-session interruption and system overlays, and a per-frame
rumble request from a core can race in before the engine's
stopped/reset handler tears down the now-stale player, so the
next CoreHaptics call throws and the uncaught exception terminates
the app.
apple_gamecontroller_joypad_set_rumble() is the hottest CoreHaptics
entry point (called every frame a core requests rumble), and none
of its player/engine calls were exception-guarded. CoreHaptics
exposes no public running-state query, so the race cannot be closed
by checking state first; wrap the acquire+play sequences in
@try/@catch instead. Covers both the device-vibration path and the
MFi-controller path; on exception we log via RARCH_ERR and skip the
rumble.
Top iOS crash for 1.22.2 (build 497): ~505 devices over the year,
trend rising. iPad is over-represented because most iPads lack a
Taptic Engine (the device path bails at the supportsHaptics guard)
but iPad + paired controller + game rumble exercises the unguarded
controller path heavily.
The Android input driver passed the translated RETROK keysym as the
character argument to input_keyboard_event(), instead of the actual
printable character. Two consequences in menu text fields:
- Modifier and lock keys (Shift, Caps Lock, Num Lock, Scroll Lock)
have keysym values >= 128. The menu line-editor maps any character
>= 128 to '?', so merely pressing these keys typed a '?'.
- Letters were never case-shifted (the keysym ignores meta state), so
capitals and shifted symbols were impossible.
Resolve the real codepoint via the device's KeyCharacterMap (mirrors
KeyEvent.getUnicodeChar()), honouring layout and shift/caps state and
yielding 0 for non-text keys. The keysym is still passed unchanged as
the key code, so core keyboard input, hotkeys and menu navigation are
unaffected.
Backwards compatible: when JNI is unavailable the character falls back
to the keysym for the ASCII range only (< 0x80), preserving the prior
lowercase behaviour while still suppressing modifier/lock keys (keysyms
>= 0x80) from leaking as '?'.
Fixes the menu capital-letter / '?' issue reported on Android.
sdl_pad_connect()'s failure path only closed pad->joypad and nulled it,
but on the SDL2 game-controller branch SDL_GameControllerOpen() can
return a valid controller handle while the open is still treated as
failed (e.g. SDL_GameControllerGetJoystick() returns NULL, so
success == false). In that case pad->controller was neither closed nor
cleared: the SDL handle leaked and pad->controller was left dangling.
This matters when a misbehaving device produces a rapid
connect/disconnect storm (reported with a GameSir T3s in Nintendo
Switch Pro mode, where SDL2 repeatedly fails to open the enumerated
gamepad): every failed connect leaked another controller handle and
left stale state in the pad slot.
Close the game controller on the SDL2 branch (which also closes the
backing joystick, so the joystick close is mutually exclusive with it),
and memset the whole pad slot to zero on failure, mirroring
sdl_pad_disconnect(). This does not fix the underlying SDL-side open
failure, but stops RetroArch from leaking handles and accumulating
stale pad state while it happens.
Compiling the C sources as C++ (CXX_BUILD=1) failed in 12 places where
the code relied on implicit conversions that C permits but C++ rejects.
All are mechanical and behaviour-preserving:
void* -> typed pointer (explicit cast):
- input/common/linux_common.c (illuminance sensor data)
- tasks/task_screenshot.c (copy.buffer -> uint32_t*)
- libretro-db/rmsgpack_dom.c (alloca result -> dom_value**)
const char* -> char* (strrchr/strchr return values):
- libretro-common/file/file_path.c (cast strrchr result; matches the
char* return contract of the C function)
- configuration.c (extra_path points at a mutable
stack buffer that is written through -> make it char*)
- menu/menu_displaylist.c (hash_type likewise -> char*)
integer -> enum (explicit cast at use site):
- runloop.c (unsigned pending_cmd -> enum event_command)
- runtime_file.c (settings uints -> last_played style/separator)
- tasks/task_database.c (int iterate result -> enum scan_verdict)
- menu/menu_driver.c (unsigned confirm_msg -> enum msg_hash_enums)
std::atomic copy:
- cheevos/cheevos.c: the static rcheevos_locals aggregate brace-init
of the two retro_atomic_int_t members used a bare '0', which under
CXX_BUILD (std::atomic<int>) selects the deleted copy constructor.
Use {0} so C++ list-initializes via the converting constructor;
for the C atomic/int/volatile backends {0} is an equivalent scalar
initializer.
With these, the tree compiles cleanly under CXX_BUILD=1 (every object
builds and the link stage is reached). This addresses the compilation
failures from #6009; note that CXX_BUILD has no CI coverage, so it will
regress again as new C-idiomatic code lands unless a build job is added.
* bug fixes to checkpoint recording and seekback
When garbage collecting rare datablocks in checkpoints is used to keep
RAM usage down, removing later blocks during seek doesn't work
properly and can cause an infinite loop. This change correctly
handles GC'd blocks rather than try to remove them again.
* bsvmovie.c: add an intfstream_seek before writing frame data
In 1abde384c8 @amcknight identified a
bug involving rewinding writable streams. This should resolve that
type of issue going forward at the cost of a no-op seek.
* sdl3: Add SDL3 Controller support
* sdl3: Add to feature list
* sdl3: Update
* Update SDL3
* Apply suggestion from @RobLoach
* Apply suggestion from @RobLoach
* sdl3: Use a different ID for sdl3_joypad.c
* sdl3: Have a standard mapping flag
* sdl3: Clean up
* sdl3: Only load mappings if the autodetect system is enabled
* sdl3: Add docs pointing to SDL3 directly
* sdl3: Probably don't need to reload the mappings before quitting
* sdl3: Safety checks and protection
* sdl3: Add SDL_SetHints() for Wii
* sdl3: LED
---------
Co-authored-by: Rob Loach <[email protected]>
Sweep of the Wii/libogc-side code:
Memory safety
- mem2: _mem2_strndup used a calloc-then-memcpy(n+1) sequence that
(a) read past s when strlen(s) < n (UB) and (b) left the result
without a NUL terminator when strlen(s) >= n. Rewritten via
strnlen() with an explicit terminator and _mem2_malloc instead
of _mem2_calloc.
- mem2: _mem2_calloc had no num*size overflow check; large pairs
silently wrapped and produced a tiny allocation that the caller
proceeded to write a huge amount into. Added the standard
UINT32_MAX/size guard.
- mem2: _mem2_memalign silently downgraded any requested alignment
to the heap's 32-byte page size. The lwp_heap can't honour
stricter requests, so reject them (return NULL) rather than lie.
- gx_joypad: gx_joypad_button returned (pad_state[port] & (1ULL<<key))
cast to int32_t, truncating bit >= 32 to zero and silently reporting
"not pressed" for every Wiimote/Classic/Nunchuk binding. Cast to
bool first.
- gx_joypad: gx_joypad_get_buttons used BITS_COPY16_PTR on a uint64_t
pad_state, dropping every Wiimote/Classic/Nunchuk bit (28..44) and
GX_QUIT_KEY (62). Use BITS_COPY64_PTR.
- gx_gfx: menu_tex.data was sized 240*212 uint32_t (203,520 bytes),
but convert_texture16 writes width*height*2 bytes into it and
gx_set_video_mode allows up to 560x240 for the 21:9 RGUI aspect
cases, producing a ~64 KB write past the buffer. Sized for the
worst-case 560*240 explicitly. The DCFlushRange call alongside
passed fb_width*fb_pitch (i.e. fb_width^2 * 2), conflating width
and height; corrected to fb_height*fb_pitch.
Leaks
- platform_gx: frontend_gx_deinit didn't release the device-thread
mutex/cond/mutex triple created in frontend_gx_init. Re-init
cycles (CMD_EVENT_QUIT + relaunch) leaked all three. Freed.
- gx_gfx: g_tex.data is memalign'd in init_vtx and was never freed
by gx_free. Now released alongside the rest of the GX state.
- gx_gfx: both XFB framebuffers in gx->framebuf[] were allocated
via MEM_K0_TO_K1(memalign(...)) in setup_video_mode and never
freed. free() takes the cached alias - use MEM_K1_TO_K0() before
passing.
Correctness
- platform_gx: DEFAULT_DIR_REMAP was filled from DEFAULT_DIR_MENU_CONFIG
*before* MENU_CONFIG itself was filled, producing a remap path
rooted at the empty/stale MENU_CONFIG value on first boot. Swapped.
- gx_joypad: check_port0_active() was called inside the per-port
loop, four times per poll, with a partial pad_count for the first
three iterations - which could force-enable port 0 spuriously.
Hoisted outside the loop.
- gx_input: rvl_input_poll's realloc-grow path zeroed only x_last/y_last
on the (possibly newly-allocated) tail entries, leaving x_abs/y_abs
uninitialised. The next poll's x_last = x_abs picked up uninitialised
heap data, producing a one-frame garbage delta on hot-plug. Zero
exactly the new tail with memset; existing entries keep their
last known position. Also dropped the shadowed inner 'unsigned i;'
and the redundant 'gx &&' on the inner if.
- gx_input: lightgun path had a video_driver_get_viewport_info(&vp)
call immediately followed by six lines that zeroed every field it
populated. The dispatcher went through the video vtable on every
lightgun query for nothing - removed.
- gx_audio: gx_audio_data was 'static volatile gx_audio_t *', making
every member access through the pointer volatile, including the
4 KB sample data[][] array (which is hot). The three IRQ-shared
fields are individually volatile-qualified in the struct, which is
sufficient. Dropped the outer volatile and the gratuitous cast in
the DMA callback. Also published gx_audio_data only after the
struct is fully initialised, so the IRQ never sees a partial state.
Folds / cleanup
- mem2: all _mem2_* helpers now static - they are only called from
the __wrap_* functions in the same TU (Makefile.wii uses
-Wl,-wrap,malloc,...). Lets the compiler inline them aggressively.
- mem2: _mem2_strdup and _mem2_strndup were using _mem2_calloc and
then immediately memcpy'ing over the zeroed bytes. _mem2_malloc
is enough.
- mem2: _CPU_ISR_Disable's "level" output was being silenced with
a (void)level; cast in __lwp_heap_block_size. Replaced with the
same `uint32_t level = 0;` idiom the rest of the codebase uses.
- platform_gx: frontend_gx_get_free_mem computed
SYSMEM1_SIZE - (SYSMEM1_SIZE - SYS_GetArena1Size())
which is just SYS_GetArena1Size(). Compiler folds it, but the
source read as if there was something subtle going on. Reduced.
- platform_gx: gx_rom_path is `extern char[]`. The `gx_rom_path &&`
arm of the if-condition is a tautology - removed.
- platform_gx: the two FRONTEND_FORK_CORE / FRONTEND_FORK_CORE_WITH_ARGS
switch arms had identical bodies - folded.
- platform_gx: device-name buffer build used two strlcpy()s; one
snprintf is the same cost and one less line.
- app_booter: two-statement (p_paddr &= 0x3FFFFFFF; p_paddr |= 0x80000000;)
bit fold; direct-init for phdrs and dolfile; collapsed
app_booter_main's exeEntryPointAddress to a conditional initialiser.
- gx_gfx: verts[], vertex_ptr[], color_ptr[], menu_gx_resolutions[]
had external linkage but were referenced only within gx_gfx.c, and
none were written. Marked static const so they go in .rodata. Call
sites to GX_SetArray (which takes void*) get explicit casts.
- gx_gfx: gx_blit_line had several declare-then-assign locals derived
from gx-> and gx_mode; folded into direct const initialisers, with
the gx NULL-check moved above the derefs.
- gx_audio: gx_audio_copy_swapped (which is not an endianness swap,
it's a 16/16 word rotate for the AI hardware's L/R lane order)
renamed to gx_audio_lr_swap and unrolled by 4 for better dispatch
on Broadway/Gekko.
Audited all 28 sites that #include <retro_endianness.h>. Three files
reference no symbol from that header (no SWAP*, swap_if_*, MSB_FIRST,
LSB_FIRST, RETRO_IS_*_ENDIAN, is_little_endian, retro_*_to_*, load32be,
store32be, retro_unaligned*, MAY_ALIAS, etc.):
input/drivers_hid/wiiu_hid.c
Uses SWAP_IF_BIG(), which is defined in joypad_connection.h and
reaches this TU transitively via wiiu/hid.h -> wiiu/input.h ->
connect/joypad_connection.h. joypad_connection.h is the site that
actually needs retro_endianness.h (for the MSB_FIRST gate on its
SWAP_IF_BIG definition); wiiu_hid.c does not.
input/input_driver.c
No endianness references at all.
tasks/task_database.c
No endianness references at all. (task_database_cue.c, included as
a sibling .c, does use MSB_FIRST and keeps its own include.)
input_driver_poll() unconditionally calls input_get_sensor_state_internal()
six times per frame to populate the per-frame sensor caches used by
shader backends. When settings->bools.input_sensors_enable is off (the
default for most users), each call:
- reads settings->bools.input_sensors_enable
- reads settings->uints.input_joypad_index[port]
- branches on the master toggle
- returns 0.0f
So 6 calls per frame return 0.0f after a handful of pointer derefs
each. Modest cost in absolute terms (low hundreds of cycles per
frame), but pure waste since the helper's return value when sensors
are disabled is fixed at 0.0f.
Short-circuit by checking the master toggle once at the caller. When
sensors are off, memset both caches to zero - which is the same
result the loop would have produced via input_get_sensor_state_internal
return values. When sensors are on, the existing six-call path runs
unchanged.
Behaviour-equivalent. No API change. Sensor-enabled path entirely
untouched.
Subtle: the previous code also zeroed the cache on the frame a user
disabled sensors mid-session, because the helper returned 0 and the
result was stored into the cache. The new code preserves that
behaviour - the memset branch fires on the first frame after disable
and zeroes the cache, so shaders see zero rather than the last
non-zero values.
The in-progress recording stream is rewound and read back to embed
in a .state file; per C99 §7.19.5.3, an input op on an update-mode
stream must precede output with a positioning call. Without this
seek, subsequent intfstream_writes from bsv_movie_next_frame
silently no-op, truncating the .replay at the SAVE_STATE.
_get_version_sdk as cross-TU APIs
These two functions were defined as static in platform_unix.c but called
from android_input.c via local static forward declarations. The setup
worked only because the Android build is the griffin unity build, which
includes both .c files into a single TU (android_input.c via the
defined(ANDROID) branch at griffin.c:700, platform_unix.c via the
defined(__linux__) branch at :1182, and Android NDK toolchains define
both ANDROID and __linux__).
Any non-griffin Android build path - or any future move of either
function - would silently fail to link. Drop the static qualifiers, drop
the bogus static forward declarations in android_input.c, and declare
proper cross-TU prototypes in platform_unix.h alongside the existing
ANDROID-only API surface (android_app_write_cmd, g_android, etc.). No
behaviour change.
video_driver_get_size and video_driver_set_size publish the size of
the area where the active video driver presents pixels to the user.
The old name says "size" without saying "size of what," and the
ambiguity has cost: the caca driver bug fixed in ad986a6 was hidden
behind exactly this confusion -- caca was passing the libretro
core's frame size to set_size when the API contract was the canvas
(surface) size, and the call site looked plausible until the field
was renamed to expose what it actually held.
Rename the public API to make the contract explicit at the call
site, and add the documentation comment that should have been on
these declarations from the start:
video_driver_get_size -> video_driver_get_output_size
video_driver_set_size -> video_driver_set_output_size
"output_size" was chosen over the alternatives:
- window_size is wrong on terminal drivers (caca, sixel) and
DRM-fullscreen-no-window
- surface_size collides with SDL_Surface and D3D surface concepts
- display_size implies the physical monitor, not the output area
- present_size is jargon and reads oddly as a noun
- viewport_size collides with the existing vp.{width,height}
aspect-corrected sub-rect
"output_size" describes purpose (size of the visible output area)
without implying mechanism, doesn't collide with existing terms,
and maps cleanly onto every driver: desktop window output, DRM
scanout output, libcaca canvas output, sixel terminal output.
The doc comment in gfx/video_driver.h spells out the per-driver
mapping (window client area for desktop GL/D3D/Vulkan/Metal,
framebuffer scanout for DRM/KMS, canvas grid for libcaca,
terminal output region for sixel), the consumer list (menu
drivers, CRT switcher, shader subsystem, input drivers, other
video drivers), threading (protected by video_st->display_lock),
and explicitly contrasts with the libretro core's frame size that
lives on each driver's local state (frame_width / frame_height
after the gl1 / fpga / gdi / network / sixel / caca renames; gl2
/ gl3 still keep their canonical video_width / video_height which
mean the backbuffer size).
74 call sites across 33 files updated by word-boundary sed; no
collisions (no other identifiers contain video_driver_get_size or
_set_size as a substring).
Validation: the file with the function definitions
(gfx/video_driver.o) and a representative sample of caller files
(menu/drivers/{xmb,ozone,materialui}.o, gfx/drivers/gl{1,2,3}.o,
gfx/video_crt_switch.o, gfx/video_shader_parse.o) all build clean
in this sandbox. The remaining 24 files hit unrelated
dependency errors (windows.h not on Linux, libcaca / libvg
headers not installed) before reaching anything rename-related;
none produced a rename-related diagnostic. Maintainer review on
a full build environment recommended before merge.
No behavior change. Pure rename + documentation.
Follow-up to 40ad5bc, applying the same intra-function deduplication
pattern to three more files where a function-top menu_st cache
already exists but inner blocks needlessly re-fetched it (or used
menu_state_get_ptr()-> chained access).
retroarch.c command_event:
- Drop 3 inner shadows of menu_st (CMD_EVENT_ADD_TO_PLAYLIST,
CMD_EVENT_RESET_CORE_ASSOCIATION, CMD_EVENT_DISK_EJECT_TOGGLE);
each is gated by #ifdef HAVE_MENU and the function-top cache
at the start of command_event is gated by the same condition,
so the inner shadows always bound the same pointer.
- Replace 5 direct menu_state_get_ptr()-> chained accesses with
menu_st-> (3 in CMD_EVENT_CLOSE_CONTENT, 1 each in
CMD_EVENT_AUDIO_STOP and CMD_EVENT_AUDIO_START). All inside
HAVE_MENU paths, so the outer cache is reachable.
- Also drop a redundant `settings` shadow inside
CMD_EVENT_ADD_TO_PLAYLIST -- the outer command_event-scope
`settings = config_get_ptr()` is unconditional and reaches
into the HAVE_MENU block.
menu/cbs/menu_cbs_ok.c generic_action_ok_displaylist_push:
- Drop 1 inner shadow in ACTION_OK_DL_DATABASE_MANAGER_LIST.
input/input_driver.c input_keyboard_event:
- Drop 1 inner shadow in the screensaver-disable block.
Total: 10 fewer calls, no semantic change (menu_state_get_ptr()
returns the same file-scope-static pointer every time, and none
of the touched files reassign menu_st anywhere).
Other multi-call sites surveyed but intentionally skipped:
general_write_handler, menu_action_handle_setting, and
runloop_environment_cb each have several sibling case-block caches
in giant switches. Promoting any of these to a function-top cache
would penalize every dispatch (most cases don't touch menu_st), so
the per-case pattern is correct there.
bsv_movie_handle_read_input_event's read path read attacker-
controlled event counts straight from the .bsv replay file
and used them as loop bounds, indexing into fixed-size
arrays:
uint8_t key_event_count; (.bsv format)
uint16_t input_event_count; (.bsv format)
bsv_key_data_t key_events[128]; (struct bsv_movie)
bsv_input_data_t input_events[512]; (struct bsv_movie)
Pre-this-patch the read loops at lines 822-836 (key) and
848-862 (input) iterated count-times into the fixed-size
arrays with no upper-bound check. A malformed .bsv could
supply:
- key_event_count = 255 -> (255-128)*sizeof(bsv_key_data_t)
= 1524 bytes of OOB heap-write past
key_events[]
- input_event_count = 65535 ->
(65535-512)*sizeof(bsv_input_data_t)
= 520184 bytes of OOB heap-write
past input_events[]
bsv_movie_t is heap-allocated via calloc in
tasks/task_movie.c::bsv_movie_init_internal so this is a
heap-buffer-overflow with attacker-chosen 8- or 12-byte
payloads at attacker-chosen offsets, producing corruption
of the rest of the bsv_movie_t struct (rewind state,
statestream pointers, save buffers) and far beyond it.
Reachability: the user opens a malformed .bsv file via
Menu -> "Load Replay File" or the --bsvplay command-line
argument. Same threat class as CDFS / CHD / BPS file-load
bugs -- requires the user to deliberately load attacker-
supplied content. Initial audit notes flagged this as
netplay-reachable but verification of network/netplay/
netplay_frontend.c shows that netplay's "replay" terminology
refers to internal frame-replay from the netplay ring buffer
and does not invoke the .bsv file-read path; this is a
file-loading bug only.
Fix: in input/bsv/bsvmovie.c, reject the file as malformed
and end playback if either count exceeds its array's
ARRAY_SIZE. A legitimate writer cannot produce more events
than the array holds because the writer's append path at
lines 489-490 / 475-476 shares the same backing array.
Adds samples/tasks/bsv_replay/bsv_replay_bounds_test as a
regression test, registered in
.github/workflows/Linux-samples-tasks.yml as an ASan-enabled
step. Four cases covering legitimate counts up to and
including the cap, and out-of-range counts being rejected
without writes.
Test follows the existing samples/tasks pattern (verbatim
copy of the post-fix predicate with maintenance-contract
comment, ASan as the discriminator). Verified to fire
under ASan when the predicate always returns true:
heap-buffer-overflow WRITE of size 1 at offset 1536 of a
1536-byte allocation in simulate_key_event_read. With the
predicate the test passes clean.
Note on writer side: the append site at line 489
movie->input_events[movie->input_event_count++] = data;
has no bound check either, but it's not fed by attacker
data and a single frame is unlikely to produce more than
512 events in practice. Not addressed by this commit; left
for a separate audit pass.
input_driver.c's poll-time remap loop indexed
mapper->analog_value[i][n] using values from
settings->uints.input_remap_ids[i][k] without proper bounds in
two places:
1. Button -> analog branch (line ~7392):
handle->analog_value[i][remap_button - RARCH_FIRST_CUSTOM_BIND] = ...
No bound on remap_button at all. Reachable values:
[RARCH_FIRST_CUSTOM_BIND, RARCH_UNMAPPED) i.e. [16, 1024),
producing remap_axis_bind in [0, 1008). Indexing past the
8-element row writes int16_t values up to ~2014 bytes past
the end of analog_value, into adjacent input_mapper_t
fields (keys[], key_button[], buttons[]) and beyond into
adjacent input_driver_state_t fields.
2. Analog -> analog branch (line ~7427):
if (remap_axis_bind < sizeof(handle->analog_value[i]))
handle->analog_value[i][remap_axis_bind] = ...
The sizeof() returns 16 (the byte size of the int16_t[8]
row), not the element count. Indices 8..15 passed the
check and OOB-wrote into the next user's analog_value row,
or past the array entirely for the last user.
Both sites read remap_button / remap_axis from
settings->uints.input_remap_ids[i][k], which is loaded from
.rmp files via input_remapping_load_file in configuration.c.
Pre-this-patch the loader fed config_get_int's result directly
into storage with no range check, so a malformed .rmp could
plant any int there. An attacker who can deliver a .rmp
alongside a ROM (a common distribution channel for
"controller config bundles") gets a 60Hz attacker-controlled
out-of-bounds write into adjacent input state -- including
the keys[] keyboard bitmask for the last user, which controls
which RetroArch hotkeys appear pressed.
Two-pronged fix:
- Load-time validation in input_remapping_load_file:
reject _remap values outside [0, RARCH_ANALOG_BIND_LIST_END)
(clamping to RARCH_UNMAPPED) before storing into
settings->uints.input_remap_ids[i][j]. Applied to both the
button-branch (j < RARCH_FIRST_CUSTOM_BIND) and the
analog-branch (j >= RARCH_FIRST_CUSTOM_BIND) of the
loader.
- Use-site bounds in input_driver.c: add an
ARRAY_SIZE-based bound to the previously-unguarded
button -> analog branch, and replace the broken sizeof()
bound with ARRAY_SIZE() in the analog -> analog branch.
Defence in depth: even if the load-time validator misses
a path (or a future caller writes to input_remap_ids[]
bypassing the loader), the use sites are now safe.
Adds samples/tasks/input_remap/input_remap_bounds_test as a
regression test, registered in
.github/workflows/Linux-samples-tasks.yml as an ASan-enabled
step. Seven cases covering the load-time clamp's accept and
reject paths, in-range writes going to the correct slot,
out-of-range writes being skipped without OOB, and the
specific historical sizeof-vs-ARRAY_SIZE bug shape.
Test follows the existing samples/tasks pattern (verbatim copy
of the post-fix predicates with a maintenance-contract comment;
ASan as the discriminator). Verified to fire under ASan when
the bound check is removed: heap-buffer-overflow WRITE of size
2 at offset 16 of a 16-byte allocation, in
do_button_to_analog_write. With the bound the test passes
clean.
Audit pass over every intfstream_close call site in production
code: out of 53 sites, 48 were already followed by free() (the
established libretro-common convention -- intfstream_close
closes the inner backend, the caller owns the struct). Five
were not, leaking 48 bytes per call.
Five fixed:
- input/bsv/bsvmovie.c:1315 check_stream (open_memory)
- input/bsv/bsvmovie.c:1643 out_stream (open_writable_memory)
- input/bsv/bsvmovie.c:1845 read_mem (open_memory)
- libretro-common/formats/cdfs/cdfs.c:433 cue_stream (alloc-fail path)
- libretro-common/formats/cdfs/cdfs.c:438 cue_stream (success path)
The bsv leaks are the more interesting set: those code paths
run on every replay verification (check_stream), every
checkpoint encode (out_stream), and every checkpoint decode
(read_mem) -- so during a long netplay session with an active
replay or in checkpointed playback the leak compounds linearly
with elapsed time. Each instance is small (48 bytes per
intfstream_t struct) but they accumulate.
The cdfs leak is per-cue-file: every time RetroArch parses a
.cue (loading a multi-track CD image, bulk database scan over
a CD library) it leaks 48 bytes. Both close paths in
cdfs_open_cue_path were affected.
Note on contract: changing intfstream_close to free the struct
itself was considered and rejected. intfstream_close ships in
libretro-common which is vendored into many cores; cores that
correctly compensate for the existing convention by calling
free() after close (the same pattern this commit standardises
on inside RetroArch itself) would silently double-free under a
contract change. Fix-the-call-sites is the only safe path.
The 48 already-correct call sites are left as-is. This commit
is a pure addition of missing free() calls; no behavioural or
cleanup-order changes.
Five unchecked alloc + immediate-deref bugs across the shared
input dispatch core and the wayland registry/data-offer
handlers.
=== input/input_driver.c: overlay_visibility calloc ===
input_overlay_set_visibility lazily allocates the visibility
array on first call:
if (!input_st->overlay_visibility)
{
unsigned i;
input_st->overlay_visibility = (enum overlay_visibility *)calloc(
MAX_VISIBILITY, sizeof(enum overlay_visibility));
for (i = 0; i < MAX_VISIBILITY; i++)
input_st->overlay_visibility[i] = OVERLAY_VISIBILITY_DEFAULT;
}
input_st->overlay_visibility[overlay_idx] = vis;
The calloc was unchecked. Both the init loop and the final
indexed write dereference input_st->overlay_visibility - OOM
was a crash on first visibility change after the overlay was
reloaded.
Fix: NULL-check and early-return. On failure overlays stay
at their compile-time default visibility, which is strictly
better than crashing. The next call re-enters this branch
and retries the allocation (input_st->overlay_visibility is
still NULL), so transient OOM is self-healing.
=== input/common/wayland_common.c: three unchecked callocs ===
wl_current_outputs_add: surface_output_t calloc
unchecked before os->output and wl_list_insert(&os->link) that
NULL-deref. Fixed with early return false - the caller
(registry handler) treats false as 'output not tracked', which
is strictly better than crashing and is self-healing on the
next wl_registry.global re-emission.
wl_registry listener wl_output branch:
display_output_t + output_info_t callocs both unchecked before
od->output = oi / oi->global_id = id / wl_output_add_listener
NULL-derefs. Fixed with joint NULL-check, free whichever
succeeded (free(NULL) is a no-op), skip adding this output to
wl->all_outputs.
wl_data_device_handle_data_offer: data_offer_ctx
calloc unchecked before offer_data->offer and wl_data_offer_
set_user_data/add_listener NULL-deref. Fixed with early
return - consequence is a lost drag-and-drop operation on
that specific offer.
=== input/common/wayland_common_webos.c: two unchecked callocs ===
wl_registry handler for webos wl_output: twin of
the wayland_common.c fix - display_output_t + output_info_t
callocs both unchecked, with the same od->output / oi->global_id
deref pattern. Same fix.
wl_registry handler for webos wl_seat:
seat_info_t calloc unchecked before si->seat / global_id / wl
writes NULL-deref. Fixed with early return - the compositor
re-emits wl_registry.global if a retry is needed.
=== Swept-clean in the same pass ===
Verified NULL-checked in the same files:
- input_driver.c other 5 sites: input_keyboard_line_append
realloc (tmp-pattern, guarded), input_remote_new calloc,
osk realloc (tmp-pattern, guarded), overlay images malloc
(has defensive num_images=0 on failure to make the
subsequent for-loop safely iterate zero times), overlay
ol calloc (prior audit commit).
- wayland_common.c wl_read_pipe realloc (tmp-pattern,
guarded, with invariant-preserving rewind on failure from
prior commit).
- wayland_common_webos.c: g_register_ctx malloc, *wwl calloc,
g_screensaver_ctx malloc, client_name malloc - all guarded.
- input/drivers_joypad/winraw_joypad.c: 4 sites guarded.
Reachability: all five fixes are reachable on OOM. The
overlay_visibility one is the most user-facing - any
visibility change hits it. The wayland ones fire on
display/seat/drag-drop events which are compositor-driven
and arbitrarily frequent.
Scope: local to each callsite; no cross-TU changes. All
existing state-mutation invariants preserved (on failure
we skip the mutation entirely rather than half-apply it).
Thread-safety: all sites run on their respective main
dispatch threads (input main thread, wayland event loop
thread). No new concurrency introduced.
Reachability timing: the overlay_visibility bug is reachable
on the very first user-invoked overlay visibility change
under OOM and survives as a latent crash for the lifetime
of the process (the allocation is lazy, retried on each
entry until it succeeds). The wayland bugs are reachable
on any wl_registry.global or wl_data_device.data_offer event
the compositor emits - which for data_offer is every
drag-and-drop enter, and for wl_output/wl_seat is every
display-plug / input-device hotplug event.
iohidmanager_hid_device_add had a dead-code NULL-guard:
for (i = 0; i < MAX_USERS; i++)
{
struct iohidmanager_hid_adapter *a =
(struct iohidmanager_hid_adapter*)hid->slots[i].data;
...
}
if (!(adapter = (struct iohidmanager_hid_adapter*)calloc(
1, sizeof(*adapter))))
return;
if (!hid)
goto error;
The 'if (!hid) goto error' guard only fires after the
for-loop has already unconditionally dereferenced hid via
hid->slots[i].data. On NULL hid the function segfaults at
the slots lookup; the later guard is unreachable.
=== Reachability ===
iohidmanager_hid_device_matched is the sole caller and fetches
hid via:
iohidmanager_hid_t *hid =
(iohidmanager_hid_t*) hid_driver_get_data();
iohidmanager_hid_device_add(device, hid);
hid_driver_get_data() can return NULL in two known windows:
1. Early init: IOHIDManager matching callbacks can fire
before iohidmanager_hid_init's return stores the hid
pointer in the global driver state. A device plugged
in (or already present) at startup triggers the race.
2. Teardown: iohidmanager_hid_free nulls the global before
tearing down IOKit subscriptions, so a matching-callback
dispatched just before the unsubscribe fires with NULL
hid.
Both windows are small but real - especially on systems where
lots of HID devices enumerate at startup (USB hubs, Bluetooth
adapters), the first window is reliably reachable.
=== Fix ===
Hoist the !hid check to the top of iohidmanager_hid_device_add,
immediately after the local variable declarations and the
device_location_id read. Remove the now-redundant later guard.
On NULL hid we return early without ever touching hid->slots or
allocating an adapter - a cleaner outcome than the previous
segfault and a strictly simpler state to reason about than the
old two-stage check (which would have leaked an allocated
adapter through goto-error on NULL hid if the dereference
crash had somehow not happened first).
=== Adapter freshness on goto error ===
The error: label at line ~946 walks adapter->hats /
adapter->axes / adapter->buttons linked lists. These reads are
safe because adapter is always calloc'd fresh (zero-filled)
before any goto error fires from this function: the four
earlier goto-error sites at the time of writing (IOHIDDeviceOpen
failure, element enumeration failures, etc.) all run after the
calloc has succeeded, so adapter is non-NULL. No change needed
there.
=== Scope ===
Single-file change. Behaviour under valid (non-NULL) hid is
unchanged. Behaviour under NULL hid goes from segfault to
silent skip of the device-add - which matches the behaviour
when the same device has already been registered (the earlier
early-return at the top of the function).
Thread-safety: IOHIDManager callbacks run on the run loop
thread, typically the main thread. No concurrent mutation of
hid itself is introduced by this change - we still rely on
the caller (hid_driver_get_data) to return a consistent
snapshot of the global.
Three bugs across two input drivers reached while sweeping
input/drivers/ for alloc-failure issues.
=== gx_input.c: UAF on mouse realloc failure (Wii/RVL only) ===
rvl_input_poll grows/shrinks the mouse array when the detected
Wiimote count changes:
gx_input_mouse_t *tmp = (gx_input_mouse_t*)realloc(
gx->mouse, count * sizeof(gx_input_mouse_t));
if (!tmp)
free(gx->mouse);
else
{
gx->mouse = tmp;
gx->mouse_max = count;
...
}
for (i = 0; i < gx->mouse_max; i++)
{
gx->mouse[i].x_last = gx->mouse[i].x_abs;
...
}
On realloc failure the old code free'd gx->mouse but left the
dangling pointer and stale gx->mouse_max in place. The
subsequent loop then iterated through freed memory via
gx->mouse[i].x_last = gx->mouse[i].x_abs - a use-after-free
at every poll until the driver was torn down, with the upper
gate 'if (gx && gx->mouse)' still accepting the freed-but-
non-NULL pointer on the next call.
Fix: on realloc failure clear gx->mouse = NULL and
gx->mouse_max = 0. The outer gate at the top of
rvl_input_poll then rejects subsequent polls, and the inner
per-poll update loop is now wrapped in 'if (gx->mouse)' so
it skips on OOM rather than iterating through NULL.
On the happy path behaviour is unchanged.
=== gx_input.c: NULL-deref on mouse calloc failure at init ===
gx_input_init unconditionally returned the gx handle even if
the per-Wiimote mouse array calloc at init time had failed:
gx->mouse_max = 1;
gx->mouse = (gx_input_mouse_t*)calloc(
gx->mouse_max, sizeof(gx_input_mouse_t));
return gx;
The RETRO_DEVICE_MOUSE input handler later does
int x = (gx->mouse[joy_idx].x_abs
- gx->mouse[joy_idx].x_last) * x_scale;
unconditionally - no 'if (gx->mouse)' guard. On OOM during
init, the very first mouse event NULL-derefs.
Fix: NULL-check the calloc and fail the whole driver init
(free(gx) + return NULL) if it fails. A Wii build running
without any mouse support is strictly better than crashing
on first Wiimote motion.
=== ps3_input.c: NULL-deref on SPU thread list malloc ===
ps3_init_spurs allocates a thread-list buffer and immediately
hands it to the SPURS SDK for population:
ps3->threads = (sys_spu_thread_t *)malloc(
sizeof(sys_spu_thread_t) * nthread);
if ((ret = spursGetSpuThreadId(ps3->spurs,
ps3->threads, &nthread)))
return ret;
malloc was unchecked. spursGetSpuThreadId writes the thread
IDs into the buffer - behaviour with NULL is
implementation-defined and at best returns an error code but
more realistically crashes inside the SDK.
Fix: NULL-check the malloc and return -1 on OOM. Matches the
return-code convention of the malloc-failure branch in
ps3_init_gem at line 364 below, which also returns -1. The
teardown path ps3_end_spurs does free(ps3->threads) which is
NULL-safe via free(NULL).
=== Swept-clean in the same pass ===
Verified via visual inspection that all other calloc/malloc/
realloc call sites in input/drivers/ are already NULL-checked:
- udev_input.c : 6 sites (3 touch state allocs, device/
realloc/driver-init) all guarded.
- winraw_input.c : 4 sites all guarded.
- rwebinput_input.c: 3 sites all guarded (realloc pattern fixed
in an earlier commit).
- qnx_input.c : 2 sites guarded.
- psp_input.c : 2 sites guarded.
- dinput.c : 2 sites guarded.
- Single-alloc drivers x11_input / switch_input / sdl_input /
ps4_input / linuxraw_input / cocoa_input / android_input :
all single init callocs guarded.
Reachability: all three fixes are reachable on OOM. The gx
UAF is the most concerning - it's triggered by routine
Wiimote hotplug on a Wii in low-memory conditions and results
in use-after-free every poll thereafter. The gx init crash
requires OOM during driver construction. The ps3 SPURS
crash requires OOM in the PS3 audio init path.
Thread-safety: all three sites are on the main input thread;
no concurrency concerns.
Scope: local to the two files modified; no API changes, no
header changes. The gx_input driver's external contract is
unchanged (init still returns NULL on failure, the poll
function still silently degrades when mouse state is
unavailable); the ps3_init_spurs function signature and error
convention are unchanged.
Five related fixes across libretro-db's runtime code (the subset
linked into RetroArch itself - c_converter.c and similar offline
tools are out of scope for this patch).
=== rmsgpack_dom.c: rmsgpack_dom_reader_state_new unchecked callocs ===
struct rmsgpack_dom_reader_state *s = calloc(1, ...);
s->i = 0; /* NULL-deref on OOM */
s->capacity = 1024;
s->growable = true;
s->stack = calloc(1024, ...);
return s;
Both callocs unchecked. The outer calloc's failure NULL-derefs
on the immediate field writes. The inner (stack) calloc's
failure is caught later when callers iterate - rmsgpack_dom_read
at line ~422 does 's->stack[0] = out' unconditionally.
Fix: NULL-check the outer calloc and return NULL. NULL-check
the inner calloc and free the outer struct + return NULL on
failure. Callers must now tolerate NULL return.
=== rmsgpack_dom.c: rmsgpack_dom_reader_state_free NULL-deref ===
free(state->stack); /* NULL-deref if state is NULL */
free(state);
state could not be NULL pre-patch (construction couldn't fail)
but with reader_state_new now able to return NULL, callers that
defer cleanup (like input/bsv/bsvmovie.c's decompress path)
would hit this free with NULL.
Fix: early return on NULL state.
=== rmsgpack_dom.c: rmsgpack_dom_reader_state_push realloc-assign-self ===
s->capacity *= 2;
s->stack = realloc(s->stack, s->capacity * ...);
if (!s->stack)
return -1;
Two stacked bugs:
1. Classic realloc-assign-self: on OOM s->stack goes NULL,
leaking the old buffer (the only reference).
2. 'capacity *= 2' happens BEFORE the realloc, so on failure
the capacity claimed the new size while the allocation is
still the old size (or NULL). The -1 return is checked by
the direct caller, but the out-of-sync state is a heap-
buffer-overflow trap if anything later ignored the error
return.
Fix: realloc-to-tmp. Compute new_capacity into a local, realloc
into new_stack, commit both only on success. Preserves the -1
OOM signal semantics.
=== query.c: query_parse_method_call zero-arg false-OOM ===
invocation->argv = (argi > 0) ? malloc(...) : NULL;
if (!invocation->argv)
{
/* emit 'OOM' error */
goto clean;
}
argv is legitimately NULL when argi == 0 (a zero-argument
function call like 'foo()') but the next block treated that as
OOM and raised the 'OOM' error string.
This was a pre-existing logic bug tangentially exposed by OOM
auditing. The observable symptom pre-patch was that any
genuinely-zero-argument function call would fail query parsing
with a bogus 'OOM' error.
Fix: gate the OOM branch on 'argi > 0 && !invocation->argv'.
Skip the memcpy when argv is NULL (nothing to copy when argi==0).
=== input/bsv/bsvmovie.c: bsv_movie_read_deduped_state consumer ===
struct rmsgpack_dom_reader_state *reader_state =
rmsgpack_dom_reader_state_new();
...
rmsgpack_dom_read_with(read_mem, &item, reader_state);
With rmsgpack_dom_reader_state_new now able to return NULL, this
caller needs to NULL-check before passing reader_state into
rmsgpack_dom_read_with (which dereferences it freely).
Fix: early bail to the existing 'exit' cleanup label on NULL.
The cleanup already calls rmsgpack_dom_reader_state_free (now
NULL-tolerant), intfstream_close (already NULL-tolerant), and
sets ret=false, so the user-visible outcome on OOM is the same
as the existing 'malformed frame' error paths.
=== Scope notes ===
Other libretro-db files checked during this pass:
* bintree.c - the single alloc site at line 35 is already
NULL-checked.
* libretrodb.c - two sites at lines 771 (buff in create_index)
and 831 (cursor_new) both use the NULL-check idiom.
* rmsgpack.c - zero raw alloc sites.
* Inner callocs in rmsgpack_dom.c:145 (dom_read_map_start) and
:173 (dom_read_array_start) use the 'if (!(items = ...))'
idiom and propagate -1 up the rmsgpack callback chain.
Not in scope (standalone offline utilities, not linked into
RetroArch runtime - see libretro-db/Makefile TARGETS):
* c_converter.c - 7 alloc sites
* libretrodb_tool.c
* rmsgpack_test.c
=== Thread-safety ===
All touched paths run synchronously on whichever thread invoked
the db query. In practice RetroArch accesses libretro-db from
the main thread (menu content scanning, explore-by-metadata)
and from task threads (background scan). No shared-state
changes; no lock discipline changes.
=== Reachability ===
* rmsgpack_dom_reader_state_new/free: the allocating variant
is only called from input/bsv/bsvmovie.c's STATESTREAM
decode path, used during savestate runahead on cores that
support bsv movie recording.
* rmsgpack_dom_reader_state_push: called recursively while
parsing deep msgpack maps/arrays inside a libretrodb .rdb
query result. Capacity doublings trigger on deeply nested
data (the default capacity is 1024 for the allocating
variant, MAX_DEPTH stack-alloca for rmsgpack_dom_read - the
stack variant sets growable=false so never reaches this
realloc path).
* query_parse_method_call: every libretrodb query string
parse - menu-driven 'Explore' category navigation and any
core's database-backed metadata lookup.
Four bugs forming one OOM cascade in the input-overlay load
pipeline. Fixing any one of them in isolation just pushes the
crash one call-frame deeper; they have to land together.
=== tasks/task_overlay.c: task_overlay_desc_populate_eightway_config ===
desc->eightway_config = (overlay_eightway_config_t *)
calloc(1, sizeof(overlay_eightway_config_t));
eightway = desc->eightway_config;
switch (desc->type)
{
case OVERLAY_TYPE_DPAD_AREA:
BIT256_SET(eightway->up, ...); /* NULL-deref */
...
Unchecked calloc; the BIT256_SET writes in the switch body
NULL-deref on OOM. void-returning function so we can't signal
failure to the caller directly.
Fix: NULL-check and early return. This leaves
desc->eightway_config = NULL for an area-type desc that needs
it, which then fires the consumer bug below.
=== input/input_driver.c: input_overlay_poll eightway dispatch ===
case OVERLAY_TYPE_DPAD_AREA:
case OVERLAY_TYPE_ABXY_AREA:
input_overlay_get_eightway_state(
desc, desc->eightway_config,
&out->buttons, x_dist, y_dist);
break;
input_overlay_get_eightway_state dereferences eightway->
slope_high unconditionally (line ~2781). With the producer fix
above leaving eightway_config NULL under OOM, this dispatch path
needs to gate on non-NULL:
if (desc->eightway_config)
input_overlay_get_eightway_state(...);
User-visible consequence on OOM: the one area whose config alloc
failed is dead until overlay reload. Strictly better than a
segfault on the next touch input.
=== tasks/task_overlay.c: task_overlay_handler data alloc ===
overlay_task_data_t *data = (overlay_task_data_t*)
calloc(1, sizeof(*data));
data->overlays = loader->overlays; /* NULL-deref */
...
Unchecked calloc in the task's 'finished and not cancelled'
branch; the seven field writes below NULL-deref on OOM.
Fix: NULL-check and early return (skip the task_set_data call).
The consumer (input_overlay_loaded in input/input_driver.c) then
sees NULL task_data, which cascades to the next bug.
=== input/input_driver.c: input_overlay_loaded task_data and ol allocs ===
overlay_task_data_t *data = (overlay_task_data_t*)task_data;
...
if (err)
return;
ol = (input_overlay_t*)calloc(1, sizeof(*ol));
ol->overlays = data->overlays; /* NULL-deref */
...
Two stacked bugs:
1. task_data can be NULL (from the producer fix above) but is
dereferenced as data->overlays on the very next usable line.
Add an early return.
2. The ol calloc is unchecked. The field writes below NULL-deref
on OOM. Unlike typical OOM-bail patterns, bailing here has a
non-trivial cleanup obligation: task_data was transferred to
us by the retro_task framework and we own it, *and* the
image_list inside it has heap texture_image attachments on
each entry's .attr.p that the success path would transfer
into ol->images via input_overlay_loaded_move_images. Since
ol doesn't exist, we need to free each texture explicitly
before string_list_free (string_list_free only frees the
list struct and its entries' .data strings, not the .attr.p
attachments).
Fix both:
if (!data)
return;
ol = (input_overlay_t*)calloc(1, sizeof(*ol));
if (!ol)
{
if (data->image_list)
{
size_t i;
for (i = 0; i < data->image_list->size; i++)
image_texture_free(
(struct texture_image*)data->image_list->elems[i].attr.p);
string_list_free(data->image_list);
}
free(data);
return;
}
=== Thread-safety ===
task_overlay_handler runs on the retro_task worker thread pool;
input_overlay_loaded runs on the main thread as the task's
completion callback. The ownership transfer of task_data via
task_set_data is already correct (producer populates, consumer
frees), so no lock discipline changes.
=== Reachability ===
Overlay loading runs every time the user picks an overlay from
the menu, configures an auto-load overlay, or switches between
overlays (e.g. OSK toggle). On handheld and embedded targets
with the memory pressure typical of running an emulator core +
overlay textures, OOM at any of these four sites is realistic.
=== Scope ===
Left image_transfer / texture loading paths in task_overlay.c
alone (the per-image malloc + image_texture_load calls all
NULL-check and goto their cleanup labels already). The four
fixes here are the OOM sites that didn't have any guard.
Two independent real OOM bugs in input-path buffer-growth code.
=== input/common/wayland_common.c: wl_read_pipe ===
The function reads one PIPE_BUF-sized chunk off a Wayland clipboard
/ drag-and-drop pipe into '*buffer', growing it per-call. The
caller's loop pattern is:
while (wl_read_pipe(pipefd[0], &buffer, length, null_terminate) > 0);
i.e. keep reading until bytes_read <= 0 (0 = EOF, <0 = error).
Pre-patch, when the malloc / realloc inside the function failed,
the else-branch of the 'if (output_buffer)' alloc check was
missing entirely. The flow was:
pos = *total_length;
*total_length += bytes_read; /* committed early */
...
output_buffer = realloc(*buffer, _len);
if (output_buffer) { memcpy(...); *buffer = output_buffer; }
/* else: fall through, return bytes_read (positive!) */
On OOM:
- The bytes we just read off the pipe are silently dropped
(temp[] is a stack buffer, overwritten on the next read()).
- *total_length has already been incremented, so the caller
believes the buffer has grown.
- *buffer is unchanged - on realloc failure it still points at
the old, smaller allocation. So the invariant 'size of
*buffer is *total_length' is broken: the recorded length is
larger than the actual allocation.
- Return value is bytes_read > 0, so the caller's while-loop
continues. Next iteration:
pos = *total_length /* over-sized */
*total_length += bytes_read
output_buffer = realloc(*buffer, _len)
If that one succeeds, the memcpy at offset 'pos' writes past
the end of what was actually in *buffer - corrupting arbitrary
heap memory if realloc gave us a buffer exactly sized to the
caller's new *total_length but missing the gap from the
failed iteration.
Fix: on allocation failure, rewind *total_length to pos (the
pre-increment value) and return -1. The caller's while-loop
then terminates; the partial data already stored in *buffer
remains valid and consistent with the (now-un-incremented)
*total_length. The current-iteration's bytes_read are lost,
which is the same failure mode as any other mid-transfer pipe
read error (not worse).
=== input/drivers/rwebinput_input.c: rwebinput_keyboard_cb ===
The Emscripten rwebinput driver grows its pending-keyboard-event
ring on demand inside the keyboard callback:
if (rwebinput->keyboard.count >= rwebinput->keyboard.max_size)
{
size_t new_max = MAX(1, rwebinput->keyboard.max_size << 1);
rwebinput->keyboard.events = realloc(rwebinput->keyboard.events,
new_max * sizeof(rwebinput->keyboard.events[0]));
rwebinput->keyboard.max_size = new_max;
}
rwebinput->keyboard.events[rwebinput->keyboard.count].type = event_type;
memcpy(&rwebinput->keyboard.events[...].event, key_event, ...);
Two bugs:
(1) 'events = realloc(events, ...)' is the classic realloc-assign-
self leak: on OOM realloc returns NULL but leaves the original
buffer valid; the self-assign overwrites the only pointer to
it, leaking the entire pending-event ring (and any events
already in it that weren't yet consumed).
(2) max_size is updated unconditionally after the realloc, before
the return value is inspected. Then the field-write and
memcpy below dereference the now-NULL events pointer - a
NULL-deref crash. Any state that survived would claim
'max_size holds 2N events' while actually holding zero.
Fix: realloc into a temp, NULL-check, and only commit both
events-pointer and max_size on success. On OOM drop this one
event on the floor and return EM_TRUE (the Emscripten callback
convention); losing a single key event in a memory-starved
browser tab is survivable - the pre-patch form was going to
crash the whole page.
=== Thread-safety ===
Neither path's locking changes. wl_read_pipe runs on whatever
thread services the Wayland event loop; rwebinput_keyboard_cb
runs on the Emscripten main thread.
=== Reachability ===
wl_read_pipe fires on every clipboard paste / drag-and-drop drop
onto a Wayland-native RetroArch window. rwebinput_keyboard_cb
fires on every keyboard event in the web build; the ring-growth
path triggers only when the input consumer falls behind (rare
under normal play, common during loading screens where the main
thread is blocked).
GCC on 10.5 PPC warned:
iohidmanager_hid.c:691: warning: comparison between signed and
unsigned
pad_connection_pad_init() at input/connect/joypad_connection.c:397
returns int32_t and can return -1 on allocation failure. Four HID
drivers store that return value in an adapter struct field and use
it for array indexing. Three got the type wrong:
iohidmanager_hid.c : uint32_t slot (warning reported here)
btstack_hid.c : uint32_t slot (same structural mismatch,
silent - no == -1 check at call site)
libusb_hid.c : int slot (compiles clean, but width is
implicit rather than explicit)
wiiusb_hid.c : int32_t slot (correct)
Align all four on int32_t, matching the return type of
pad_connection_pad_init and wiiusb_hid.c's precedent.
Array-indexing call sites (hid->slots[adapter->slot], hid->hats
[adapter->slot][...], etc.) are unaffected - signed and unsigned
integer types index arrays identically through integer promotion
to ptrdiff_t. The only behavioural effect is that slot == -1
checks (iohidmanager_hid.c:691, libusb_hid.c:313) are now signed/
signed comparisons and actually do what they look like they do.
Previously the iohidmanager check "worked" only because the
promotion rules happen to coerce (int)-1 to UINT32_MAX for the
comparison - accidental correctness.
GCC 4.0 (Xcode 3.1, PowerPC) reports:
cocoa_input.m:1062: error: invalid initializer
Two problems compound in this four-line sequence on 32-bit 10.5:
CGPoint window_pos = window.frame.origin;
CGSize window_size = window.frame.size;
First, NSWindow's frame is declared - (NSRect)frame; on the
10.5-10.9 SDKs, not as @property. GCC 4.0 only supports Obj-C 2.0
dot-syntax on explicitly declared @property, so window.frame fails
(same class of issue as the keyWindow / alphaValue fixes in
c49a525).
Second, window.frame.origin evaluates to NSPoint, but the variable
is typed CGPoint. On 32-bit Darwin, NSPoint and CGPoint are
separate, incompatible types — Apple only unified them on LP64.
Same for NSSize vs CGSize on the next line.
Rewrite to fetch the frame once via bracket syntax into an NSRect
local, then feed the float field values directly to CGPointMake.
CGWarpMouseCursorPosition gets the same CGPoint as before; no
runtime change. Modern clang compiles identically.
The bundled DirectX headers in gfx/include/dxsdk/ are modern Windows
8.x/10 SDK headers and use SAL annotations throughout their function
prototypes. These annotations come in two generations:
* SAL 1 (double-underscore): __in, __out, __inout, __in_ecount,
__out_bcount, __deref_out, etc.
* SAL 2 (leading underscore + trailing underscore): _In_, _Out_,
_Inout_, _In_reads_(s), _Out_writes_bytes_(s), _COM_Outptr_,
_Always_(x), _Outptr_opt_result_maybenull_, etc.
Older Microsoft toolchains ship incomplete or nonexistent sal.h:
* MSVC 2003 (_MSC_VER 1310): no sal.h at all. Both SAL 1 and SAL 2
macros are undefined.
* MSVC 2005 (_MSC_VER 1400) and MSVC 2008 RTM (_MSC_VER 1500): sal.h
has SAL 1 only.
* MSVC 2008 SP1 and MSVC 2010 (_MSC_VER 1600): sal.h ships a partial
SAL 2 set (_In_, _Out_, _Inout_, etc.) but is missing the
buffer-size, COM-outptr, and wrapper families.
* MSVC 2012+ (_MSC_VER >= 1700): full SAL 2 in sal.h.
Without definitions for these macros, the preprocessor treats them as
unknown identifiers and every function declaration that uses one fails
to parse, producing cascades like:
error C2143: syntax error : missing ')' before 'type'
error C2081: '_COM_Outptr_' : name in formal parameter list illegal
Add gfx/include/dxsdk/dxsdk_sal_compat.h which stubs out the missing
SAL 1 and SAL 2 macros to empty (or to a pass-through for wrapper forms
like _Always_(x) / _When_(c,x)) so declarations using them parse as
plain C. The shim is structured in four tiers:
* Skip the whole thing on MSVC 2012+ via _MSC_VER < 1700 gate.
* Skip #include <sal.h> on MSVC 2003, where the header does not
exist, via a _MSC_VER >= 1400 gate.
* Stub the SAL 1 family, guarded per-macro with #ifndef so MSVC 2005
through 2010 (which already have them) leave them untouched and
MSVC 2003 gets working definitions.
* Stub the SAL 2 family the same way, filling in whatever the
toolchain is missing without redefinition warnings.
Every macro is wrapped in #ifndef to avoid C4005 redefinition warnings
on toolchains that ship a partial SAL set.
Include the shim from the three bundled headers that tripped MSVC 2008
in griffin builds:
* gfx/include/dxsdk/d3dcommon.h
* gfx/include/dxsdk/d3d11shader.h
* gfx/include/dxsdk/d3dcompiler.h
The shim's outer include guard makes repeat inclusion free, so if
additional DX headers surface the same errors in future build
configurations they can be added with a one-line #include.
This is a build-only fix. The stubs discard static-analysis
information, which older MSVC versions could not consume anyway.
No behavior change on any supported toolchain.
Adds an early-exit check at the top of input_joypad_analog_button(). After resolving the axis binding (checking both user bind and autoconfig fallback), if the result is AXIS_NONE, the function skips directly to the digital button check — bypassing the input_joypad_axis() function call, the deadzone magnitude computation, and the drv->axis() indirect call that would all produce zero anyway.
Why it helps: In the remap loop, input_joypad_analog_button() is called for every pressed button. During typical gameplay, 4–8 buttons are pressed per user (d-pad directions, face buttons, shoulders). Most of these are purely digital — they have joyaxis == AXIS_NONE in both the user bind and autoconfig. The original code called into input_joypad_axis() regardless, which evaluated the joyaxis != AXIS_NONE ternary (returning 0), then went through deadzone checking (returning 0 since normal_mag is 0), then fell through to the digital button check. The early exit skips all of that.
What's eliminated per digital button: One input_joypad_axis() function call (which itself would call drv->axis() if not for the AXIS_NONE guard), the deadzone normal_mag comparison, and the sensitivity scaling check. For a button that does have an analog axis, the behavior is completely unchanged — the early exit is not taken.
B8: Bitmask cache for old-style cores — CLEAR WINNER (2.0×, 1314 ns @ 16 users)
This is now the single largest remaining optimization. When cores query buttons individually (not using RETRO_DEVICE_ID_JOYPAD_MASK), each of the 16 button queries goes through input_state_wrap → joypad->button() + joypad->axis() — 32 indirect calls per port. A per-port bitmask cache computed once at poll time (or lazily on first query) and served for subsequent individual queries would cut this to 1 joypad->state() call plus 16 bit lookups. Implementation: add a uint32_t joypad_state_cache[MAX_USERS] and bool joypad_cache_valid[MAX_USERS] to input_driver_state_t, invalidate at poll start, populate on first JOYPAD_MASK or individual query per port, serve subsequent queries from cache. The optimization only benefits cores that use the old per-button query pattern — modern cores already use JOYPAD_MASK internally.
Analog deadzone axis caching (HIGHEST PRIORITY — 2.5× measured speedup)
The input_joypad_analog_axis function is called 4 times per user per frame
(2 sticks × X/Y). Each call reads all 4 axis values via drv->axis() to
compute a radial deadzone magnitude — but X and Y of the same stick
produce identical magnitude. The fix: restructure the remap loop's
analog section to call a new input_joypad_analog_stick() that processes
both axes of a stick together, reading the 4 raw axis values once,
computing sqrtf once, and applying deadzone/sensitivity to both X and Y
from cached raw values.
Measured savings: 671 ns/frame at 16 users (2.48× speedup on the analog path alone),
644 ns saved on the full poll (1.44× aggregate).
What was done: Added a new input_joypad_analog_stick() function (215 lines) that processes both X and Y axes of a single analog stick together, computing the radial deadzone magnitude only once. The remap loop's inner j loop (which called input_joypad_analog_axis 4 times per user — twice per stick, redundantly computing magnitude each time) was replaced with 2 calls to the new per-stick function.
What was eliminated per stick per user per frame:
4 redundant drv->axis() indirect calls (the Y-axis pass re-reads the same 4 raw values that X already read for magnitude)
1 redundant sqrtf() (X and Y of the same stick have identical radial magnitude)
6 redundant bind lookups and validity checks (the Y pass re-resolves all 6 bind pointers that X already resolved)
1 redundant input_conv_analog_id_to_bind_id macro expansion
Per frame totals eliminated: For each user with 2 sticks: 8 drv->axis() calls, 2 sqrtf() calls, 12 bind lookups, 2 macro expansions. At 16 users: 128 indirect calls and 32 sqrtf calls eliminated.