Commit Graph
35 Commits
Author SHA1 Message Date
Matt Akins 42a6c7f167 vulkan: support swapchains with more than eight images (#19186) 2026-07-12 12:39:30 +02:00
libretroadmin 4da610d390 samples/database: stub msg_hash_us_index_init
The msg_hash string-table refactor made msg_hash_set_uint() call
msg_hash_us_index_init() (from intl/msg_hash_us.c) under MSG_HASH_HAVE_STRTAB,
which is enabled for every non-MSVC compiler. The database sample links
msg_hash.o but deliberately stubs the intl layer (msg_hash_to_str_us,
msg_hash_get_help_us_enum) rather than pulling in the ~1 MB of string tables,
so the new call left an undefined reference at link time.

Add a matching no-op stub for msg_hash_us_index_init. With msg_hash_to_str_us()
already stubbed to return "", the string-table index is never consulted, so the
empty body is correct and the sample still avoids intl/msg_hash_us.c.
2026-07-02 13:22:52 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre 4e26c8875a gfx/gfx_widgets: lock msg_queue against producer-producer race
dispgfx_widget_t::msg_queue is a fifo_buffer_t storing
disp_widget_msg_t* pointers.  Pre-fix it had no dedicated lock,
yet the producer (gfx_widgets_msg_queue_push) is reachable from
threads other than the main thread, and the lock that *did*
exist (current_msgs_lock) protects current_msgs[] -- the
displayed-message deque -- not the FIFO.

Producer reachability:

  1. runloop.c:runloop_msg_queue_push.  Holds RUNLOOP_MSG_QUEUE_
     LOCK across the call.  Reachable from 40+ files; the
     threaded task system at libretro-common/queues/task_queue.c
     runs a worker thread that pushes via this entry point.
  2. runloop.c:runloop_task_msg_queue_push.  Same lock.
  3. gfx/video_driver.c:video_driver_frame.  Acquires
     RUNLOOP_MSG_QUEUE_LOCK to drain runloop_st->msg_queue,
     RELEASES it, then calls gfx_widgets_msg_queue_push without
     the lock.

Path 3 runs on the main thread; path 1 can run on any thread.
RUNLOOP_MSG_QUEUE_LOCK guards runloop_st->msg_queue, NOT
p_dispwidget->msg_queue -- the lock name reflects the runloop
struct it was named after.  So path 3's fifo_write (no lock)
can race with path 1's fifo_write (lock held but irrelevant
to path 3).

The consumer (gfx_widgets_iterate's fifo_read) is called from
runloop.c:6048 inside RUNLOOP_MSG_QUEUE_LOCK and from
gfx_widgets.c:973 inside current_msgs_lock -- but neither lock
is held by path 3, so consumer fifo_reads can observe a half-
updated `end` cursor from path 3's fifo_writes.

Outcome of a race: torn `end` cursor publishes a bad pointer
in the disp_widget_msg_t* slots.  The consumer dereferences
that as disp_widget_msg_t* in gfx_widgets_iterate, producing
use-after-free / segfault.  On x86 TSO the race is masked most
of the time by hardware ordering; on Win-on-ARM / Apple
Silicon (AArch64) it surfaces more often.

Collision probability is low (~1 per 14 hours of normal use,
estimated from frame rate * task message rate * fifo_write
window) but the worst case is crash-class.

Fix
---

Add slock_t* msg_queue_lock to dispgfx_widget_t.  Acquire it
around every fifo_write (in gfx_widgets_msg_queue_push), every
fifo_read (in gfx_widgets_iterate), and the avail-check that
gates each one.  msg_queue_lock is the inner lock when both
it and current_msgs_lock are held; lock order is consistent
across all call sites.

The outer FIFO_*_AVAIL fast-path checks that used to wrap the
producer and consumer function bodies have been dropped.
Reading the FIFO cursors outside msg_queue_lock is itself a
data race against the locked writes -- TSan-detectable; benign
on x86 TSO but real on weak-memory hardware.  The locked
re-check at the actual fifo_write / fifo_read site is the
correctness gate.

Removing the producer's outer gate also fixes a latent
behaviour bug: the update-existing branch (else clause in
gfx_widgets_msg_queue_push) does not write to the FIFO -- it
only mutates an already-tracked widget -- so suppressing it on
FIFO-full was wrong.  Existing widgets now get updated
regardless of FIFO state.

The producer's spawn-new branch may now race-lose the avail
re-check after allocating msg_widget (concurrent producer
filled the last slot between unlocked allocation and locked
write).  In that case the function frees the just-allocated
widget via gfx_widgets_msg_queue_free + free and returns.  A
forward declaration of gfx_widgets_msg_queue_free is added near
the top of the file because that function is defined further
down.

Regression test
---------------

samples/gfx/gfx_widgets_msg_queue_race/ mirrors the post-fix
locking discipline of gfx_widgets_msg_queue_push and
gfx_widgets_iterate's FIFO interaction in isolation.  Two
producer threads + one consumer thread + 500K iterations per
producer; smoke check verifies single-threaded FIFO order.

Wired into .github/workflows/Linux-samples-gfx.yml as a TSan
lane (CC=clang, SANITIZER=thread, TSAN_OPTIONS=halt_on_error=1)
following the gfx_thumbnail_status_atomic_test convention.
TSan instruments every memory access on fifo_buffer_t::end and
::first, so a future regression that drops the lock around any
of the producer / consumer / avail-check sites will be flagged
as a data race and fail the workflow.

Verified locally
----------------

  - samples/gfx/gfx_widgets_msg_queue_race builds and runs
    clean as C (gcc -O0), C++11 (g++ -std=c++11 -xc++), and
    Clang under -fsanitize=thread.
  - TSan with halt_on_error=1: clean exit (post-fix lock
    discipline causes no observed races across 500K * 2
    iterations).
  - Mutation test: removing the slock_lock/unlock around the
    producer's fifo_write makes TSan fire on the first
    collision, validating that the test is sensitive to the
    exact regression we're guarding against.
  - gfx/gfx_widgets.c syntax-checks clean with HAVE_THREADS +
    HAVE_GFX_WIDGETS via gcc -fsyntax-only against the
    libretro-common headers.
2026-04-29 23:27:44 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre ac04c9189b gfx/gfx_thumbnail: port hand-rolled atomic shim to retro_atomic.h
The file had its own local __atomic_* / _Interlocked / volatile
shim guarding the cross-thread .status field, written before
retro_atomic.h existed.  Replace it with the portable API now
that the header is upstream (75ea1e4).

The shim was 18 lines, well-commented, correct, and operated on
plain `int*` via GCC's __atomic_* extension form.  The C11
stdatomic and C++11 std::atomic backends in retro_atomic.h cannot
operate on plain pointers -- their typedefs are _Atomic int and
std::atomic<int>, which forbid plain dereference -- so this port
changes the .status field type from `enum gfx_thumbnail_status`
to `retro_atomic_int_t` and routes every read and write through
retro_atomic_*_int.  Twenty-five access sites are converted; all
but three are single-threaded main-thread accesses that were
already non-atomic in shape, but now go through the API because
the field's atomic type requires it.  On x86 TSO the
acquire/release barriers compile out entirely; on weak-memory
ARM/PowerPC the cost is one extra ldar/stlr per cold-path
access (menu list updates, frame draws -- never per-sample).

The two cross-thread sites that drove the original shim are
preserved verbatim:
  - gfx_thumbnail_handle_upload publishes texture/width/height
    via release-store of AVAILABLE.
  - gfx_thumbnail_draw acquire-loads the status before reading
    the texture/width/height.
Both already had explanatory comments; those are kept.

Field-type ABI: retro_atomic_int_t is int-sized and int-aligned
on every supported backend, so gfx_thumbnail_t's layout is
unchanged.  Verified across 7 backends and 2 architectures
(x86_64, AArch64); also asserted at compile-time in the new
test (see below).

Intra-file initialisation of stack-local gfx_thumbnail_t in
gfx_thumbnail_draw uses retro_atomic_int_init() for the .status
field; plain assignment to an _Atomic int is illegal under C11
stdatomic.

CXX_BUILD compatibility
-----------------------

RetroArch's CXX_BUILD mode (Apple platforms, etc.) compiles
every .c file as C++.  Once .status is std::atomic<int>, two
C-valid patterns become C++-invalid:

  - memset(&t, 0, sizeof(t)) of a struct containing
    std::atomic<int> warns -Wclass-memaccess (the struct is no
    longer trivially-copyable).
  - `*dst = *src` for a struct embedding std::atomic<int>
    fails to compile (the deleted copy-assignment of
    std::atomic<int> propagates upward through aggregates).

The first case fires once: xmb.c:667's xmb_alloc_node() did
memset(&node->thumbnail_icon.icon, 0, ...) instead of calloc'ing
the whole node, intentionally to avoid zeroing a multi-KB
thumbnail_path_data substruct on the hot path.  Replaced with
gfx_thumbnail_init_blank() -- a new field-by-field zero-init
helper added to gfx_thumbnail.h.  The second case does not
fire: the only struct copy in the menu drivers
(ozone.c:4652 ozone_copy_node) operates on ozone_node_t, which
does not embed gfx_thumbnail_t (the gfx_thumbnail_t fields are
in ozone_handle_t, which is calloc'd, never struct-copied).

calloc-init of structs containing std::atomic<int>
(xmb_handle_t, ozone_handle_t, materialui_handle_t) is
technically UB per C++11 [atomics.types.generic] (the default
constructor leaves the atomic uninitialised; reads should be
preceded by atomic_init).  In practice every libstdc++/libc++/
MSVC STL treats a zero-bit pattern of std::atomic<int> as a
zero-valued atomic, and reads via atomic_load_explicit return 0.
Verified with all three menu drivers under CXX_BUILD.

Wrappers (GFX_THUMB_STATUS_STORE / GFX_THUMB_STATUS_LOAD) are
preserved as static-inline functions rather than function-like
macros.  GCC 13+ at -O3 emits a -Wstringop-overflow false-
positive on the inlined __atomic_* primitive when called from
gfx_thumbnail_request -- the optimiser cannot prove the
@thumbnail argument non-NULL across every `goto end:` flow-graph
path.  The pre-port code did not trip this because the shim's
explicit (int*) cast on enum* defeated GCC's analysis;
retro_atomic_int_t is int directly on the GCC backend, so no
cast remains to do that.  The wrappers carry a targeted
_Pragma("GCC diagnostic ignored \"-Wstringop-overflow\"") that
suppresses the warning at the function boundary; codegen is
unchanged on every backend (verified via aarch64 objdump:
14 ldar / 14 stlr emitted, identical to pre-port).  Other
toolchains (clang, MSVC, older GCC) skip the suppression.

Regression test
---------------

samples/gfx/gfx_thumbnail_status_atomic/ exercises the
synchronisation pattern in isolation.  The test does not link
gfx_thumbnail.c; it mirrors the relevant types and macros and
runs an SPSC producer/consumer stress.

Verified failure mode: deliberately moving the producer's
generation-counter bump before the data writes (simulating a
removed release-store fence) makes the stress fail with
hundreds of mismatched reads on x86 and a "ThreadSanitizer:
data race in consumer_thread" report under TSan
(halt_on_error=1 -> exit 66).  Restoring the fence gives 1M
iterations with 0 mismatches and a clean TSan run.

Static checks asserted at compile time:
  - sizeof(gfx_thumbnail_t.status) == sizeof(int), so the
    field-type change preserves struct layout.
  - HAVE_RETRO_ATOMIC is set after the include.

Runtime checks:
  - All four GFX_THUMBNAIL_STATUS_* values round-trip through
    GFX_THUMB_STATUS_STORE / GFX_THUMB_STATUS_LOAD.
  - 1M-iteration SPSC handshake stress (HAVE_THREADS only):
    producer stages (texture, width, height) = (i, i, i),
    stores AVAILABLE via the macro under test, bumps a
    `produced` counter via release-store; consumer waits for
    the new generation, reads the four fields via the macro
    under test, verifies all four equal i, acks via a
    `consumed` counter.  The handshake prevents the producer
    from rewriting the data while the consumer is reading it;
    without it, torn reads would be expected even with correct
    barriers (a property of the test design, not the
    primitives).

The test mirrors gfx_thumbnail_init_blank() locally rather than
including gfx_thumbnail.h, since the test deliberately shadows
the header's type definitions to verify layout invariants.

CI
--

.github/workflows/Linux-samples-gfx.yml gets one new step that
builds the sample with SANITIZER=thread and runs it with
TSAN_OPTIONS=halt_on_error=1.  TSan is the right validator for
this test specifically: missing-barrier regressions show up as
data races even on x86 TSO, where the hardware would otherwise
hide them at runtime.  ASan/UBSan would catch nothing here
(no out-of-bounds access, no UB), so they are not added.

Verified locally
----------------

Compile-clean across:
  - gcc -O2 / -O3 -Wall -Werror, x86_64, all 5 affected files
    (gfx_thumbnail.c, xmb.c, ozone.c, materialui.c, runloop.c)
  - g++ -xc++ -std=c++11 -DCXX_BUILD, same 5 files
  - clang -O2, same 5 files
  - aarch64-linux-gnu-gcc + qemu-user, gfx_thumbnail.c
  - arm-linux-gnueabihf-gcc, gfx_thumbnail.c
  - clang -x objective-c / -x objective-c++ smoke (header-only
    inclusion check; mirrors cocoa_common.m's transitive path
    via menu_driver.h)
  - C89 -ansi -pedantic, gfx_thumbnail.c
  - Forced backends: C11 stdatomic, GCC __sync_*, volatile
    fallback
  - Clang static analyser
  - HAVE_THREADS=0 (test sample, static checks only)

Functional:
  - 1M-iter SPSC stress passes on x86_64 native
  - 1M-iter SPSC stress passes under qemu-aarch64
  - TSan halt_on_error=1 catches deliberately-injected
    missing-barrier bug -> exit 66
  - aarch64 objdump shows 14 ldar / 14 stlr in
    gfx_thumbnail.o, identical between pre-port and post-port
  - x86 codegen: plain mov for cross-thread sites (TSO-correct)
  - ARM32 codegen: 28 dmb instructions emitted

Behavior when no real atomic backend is available
-------------------------------------------------

retro_atomic.h's cascade always selects something; the last
tier is a volatile fallback (`#warning` unless suppressed,
`RETRO_ATOMIC_LOCK_FREE` undefined).  On that tier, expansions
are plain volatile loads and stores -- no memory barriers --
which is correct only on single-core targets or x86 TSO.  The
header's docstring documents this and the intended caller-side
gate is `#if defined(RETRO_ATOMIC_LOCK_FREE)`.

gfx_thumbnail does NOT add such a gate, matching pre-port
behavior: the original shim's `#else` branch was also a plain
volatile read/write with no barriers, and gfx_thumbnail never
checked whether barriers were available.  That tradeoff was
explicit in 91166d0 ("Note on correctness"), where the
volatile fallback's SMP PowerPC G5 weakness was acknowledged
and accepted: the failure mode is at worst a fade-in
animation starting one frame late, not corruption (status is
a single aligned int -- no torn reads possible -- and a
cosmetic mistime is recoverable on the next frame).

What changes with the port: the cascade gets richer.  Pre-
port, anything that wasn't the GCC __atomic_* or MSVC
Interlocked* branch landed on the volatile fallback.  Post-
port, four additional tiers sit between modern GCC and
volatile -- C11 stdatomic, C++11 std::atomic, Apple OSAtomic*,
GCC __sync_* -- which means every real RetroArch target with
threaded video falls into a tier that gives proper release/
acquire ordering.  The G5 case the historical note
acknowledged is now covered by GCC __sync_* (which existed in
GCC 4.1+ and was named as the proper fix if anyone wanted to
harden the G5 path later); G5 with modern devkitPPC would land
on __atomic_* and get real lwsync.

Net: on every platform where atomics are "unavailable" in the
sense of "no real backend matches", pre-port and post-port
emit identical plain-volatile codegen (verified by aarch64-
gcc -DRETRO_ATOMIC_FORCE_VOLATILE: gfx_thumbnail_handle_upload
emits plain `str` rather than `stlr`, same as pre-port's
volatile branch on the same target).  The port introduces no
new "atomics unavailable" footgun.

Not verified on real hardware:
  - MSVC ARM64 (relies on retro_atomic.h's MSVC backend, which
    landed in 75ea1e4 and is itself not yet hardware-validated)
  - Real PowerPC SMP (Wii U, Xbox 360)
  - End-to-end Apple build (macOS / iOS / tvOS)
2026-04-29 20:16:06 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre 832250be0a Buildfix for CI 2026-04-28 17:37:11 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre dd3204d086 gfx/drivers_context/{wayland,w,x}_vk_ctx: fix UAF + double-free + CI
Found during the audit of the Vulkan context shims after f5c7df5.
Adds an AddressSanitizer regression test under samples/gfx/
vulkan_ctx_double_free/ wired into the Linux-samples-gfx CI
workflow as a sixth step.

Three of the five Vulkan context drivers' set_video_mode hooks
called their own destroy() (or destroy_resources() + free()) on
ctx_data before returning false:

  wayland_vk_ctx.c:218  gfx_ctx_wl_destroy(data)
  w_vk_ctx.c:229        gfx_ctx_w_vk_destroy(data)
  x_vk_ctx.c:489-499    destroy_resources + free

The caller in gfx/drivers/vulkan.c::vulkan_init treats a false
return from set_video_mode (line 4938) as a failed in-flight
vk_t construction and `goto error`s into vulkan_free() (line
5120), which at line 4665 calls

   vk->ctx_driver->destroy(vk->ctx_data);

-- on the very pointer the inner set_video_mode just freed.
The ctx_driver->destroy hook then walks the freed struct
(gfx_ctx_wl_destroy_resources / gfx_ctx_x_vk_destroy_resources
dereference fields off the freed pointer) and free()s the same
pointer a second time.

On glibc with default malloc the second free is detected
(`double free or corruption`) and the process aborts.  Under
jemalloc / mimalloc / older glibc the second free silently
corrupts the heap, with a write-what-where primitive controlled
by the allocator's freelist layout.

Reachability: vulkan_surface_create() failure (missing extension
or driver-level Vulkan error), Wayland's set_video_mode_common_*
helpers failing, X11's XGetVisualInfo returning NULL or
x11_input_ctx_new failing, win32_set_video_mode failing.  None
are common but all are reachable on misconfigured systems,
headless tests, or CI environments without a working display.

* gfx/drivers_context/wayland_vk_ctx.c
* gfx/drivers_context/w_vk_ctx.c
* gfx/drivers_context/x_vk_ctx.c

  Drop the destroy/free call from each set_video_mode error
  path.  Cocoa (cocoa_vk_ctx.m) and Android (android_vk_ctx.c)
  already implement this correctly: just `return false` and
  let the caller's vulkan_free chain do the single cleanup.
  This makes the other three match.

  X11's local XFree(vi) and `g_x11_screen = 0` reset are
  preserved -- those are local-resource and global-state
  cleanups respectively, not duplicates of the upper-layer
  destroy.  Wayland and Win32 had no such locals to preserve.

  Each error-path edit carries an inline comment naming the
  upper-layer cleanup site (gfx/drivers/vulkan.c::vulkan_free
  line 4665) so a future maintainer who wonders why the inner
  destroy is absent doesn't reintroduce it.

* samples/gfx/vulkan_ctx_double_free/,
  .github/workflows/Linux-samples-gfx.yml

  Regression test following the convention of the v3 vulkan/
  tests, the v4 slang test, the v5 mailbox-leak test, and the
  security regression tests under samples/tasks/.  Models the
  lifecycle contract abstractly rather than building any one
  driver -- the bug is the shape of the cleanup flow, not a
  driver-specific quirk.

  Five probes: post-fix success path (1 alloc / 1 destroy);
  post-fix failure path (the smoking gun: 1 alloc / 1 destroy,
  no double-free, no UAF); 16 back-to-back failures (no
  accumulation, no leaks); interleaved success/failure; and a
  documentation probe describing how a maintainer can swap the
  function pointer to re-trigger the pre-fix UAF.

  Verified pre/post-patch discrimination: under the pre-fix
  shape ASan reports `double-free` at the upper-layer destroy
  with the freed allocation traced back to set_video_mode's
  inner destroy.  Under the post-fix shape all five probes
  pass clean.

  Wires into Linux-samples-gfx.yml as a sixth step, modeled on
  the existing five with the same per-step explanation of what
  the test is regression-testing and the "verbatim copy must
  follow" reminder.
2026-04-28 15:23:16 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre f5c7df53c6 gfx/common/vulkan: plug partial-init leak in emulated mailbox + CI
Found during the v2 audit of gfx/common/vulkan_common.c, deferred
from the v3 patch (da5e650) which addressed the three high-severity
heap-corruption findings.  Adds an AddressSanitizer + LSan
regression test under samples/gfx/vulkan_mailbox_init_leak/ wired
into the existing Linux-samples-gfx CI workflow as a fifth step.

vulkan_emulated_mailbox_init has three sequential allocations
(scond_new, slock_new, sthread_create) and on any of the latter
two failures returns `false` directly, leaking the already-
allocated cond and/or lock.  Both production call sites in
vulkan_create_swapchain ignore the return value -- so an init
failure also leaves vk->mailbox.lock == NULL and vk->mailbox.cond
== NULL while VK_DATA_FLAG_EMULATING_MAILBOX is still set,
setting up a NULL-deref the next time vulkan_acquire_next_image()
routes into the emulated path (slock_lock(NULL) inside
vulkan_emulated_mailbox_acquire_next_image()).

The leak itself is small (the libretro-common slock_t / scond_t
each hold a pthread mutex / cond worth of data); the trigger
requires scond_new or slock_new to fail, which means OOM or
pthread resource exhaustion.  Realistic mainly on memory-
constrained devices (older Android, Vita, 3DS, Wii U) where
filter-chain teardown / re-init under thrashing memory pressure
can put the system in this state.

* gfx/common/vulkan_common.c

  Route every early-failure path in vulkan_emulated_mailbox_init
  through `goto error` to a single cleanup that calls
  vulkan_emulated_mailbox_deinit().  The deinit function is
  null-safe (each free is guarded by an `if (mailbox->X)` check)
  and ends with a memset, so the deinit-on-failure shape matches
  the deinit-on-shutdown shape exactly -- the two production
  callers that ignore the return value get a self-healing
  failure mode.

  The post-deinit memset also clears mailbox->swapchain, which
  trips the existing guard at vulkan_acquire_next_image:

     if (vk->mailbox.swapchain == VK_NULL_HANDLE)
        err = VK_ERROR_OUT_OF_DATE_KHR;

  closing the NULL-deref crash that the bare `return false` left
  open.  No caller-side changes needed.

* samples/gfx/vulkan_mailbox_init_leak/,
  .github/workflows/Linux-samples-gfx.yml

  Regression test following the convention of the v3 vulkan/
  tests, the v4 slang test, and the security regression tests
  under samples/tasks/.  Keeps verbatim copies of both
  vulkan_emulated_mailbox_init() and vulkan_emulated_mailbox_
  deinit() demarcated by `=== verbatim copy ===` markers, with
  an `IMPORTANT: ... must follow` comment.  Plain C, asserts
  manually, exits nonzero on failure.

  Five probes: scond fails first (was already correct, baseline
  case); slock fails second (pre-fix: scond leak); sthread fails
  third (pre-fix: scond + slock leak); full success (sanity check
  on alloc/free pairing post-deinit); and an explicit
  __lsan_do_recoverable_leak_check() call after running all
  three failure stages.  Mocks scond_new / slock_new /
  sthread_create with controllable failure injection so each
  stage can be exercised deterministically.

  Verified pre/post-patch discrimination: under the pre-fix
  `return false` shape, ASan's LeakSanitizer reports 384 bytes
  leaked across 6 allocations (1 scond from slock-fails + 2
  alloc/leak pairs from sthread-fails + 3 from the lsan-check
  probe), each with a full backtrace pointing into
  vulkan_emulated_mailbox_init.  Under the post-fix shape all
  five probes pass clean.  The CI step runs with
  ASAN_OPTIONS=detect_leaks=1 so leaks fail the run with a
  non-zero exit.
2026-04-28 14:58:10 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre 29080418c3 gfx/drivers_shader: cap arrayed texture-semantic indices in slang + CI
Found during a continued audit of gfx/drivers_shader/shader_vulkan.cpp
and slang_process.cpp after the v3 vulkan-driver fixes (da5e650).
Adds an AddressSanitizer regression test under samples/gfx/
slang_texture_index_bounds/ wired into the existing Linux-samples-gfx
CI workflow as a fourth step.

slang_process.cpp's add_active_buffer_ranges() and the direct sampler-
binding loop in slang_reflect() both consume an unsigned array_index
parsed from the suffix of arrayed semantic names like
`OriginalHistory42` / `PassFeedback9` / `User7`.  The parsing happens
in slang_name_to_texture_semantic_array() via strtoul with no upper
bound.  That index then flows into resize_minimum() at slang_process.c
line 1116 and the direct-binding write at line 1701, growing
reflection->semantic_textures[sem] to (index + 1) entries.

PASS_OUTPUT was already bounded against reflection->pass_number, with
a dedicated "Non causal filter chain detected" diagnostic;
ORIGINAL_HISTORY, PASS_FEEDBACK and USER had no upper bound.

A malicious slang shader declaring

   layout(set = 0, binding = 1) uniform sampler2D
      OriginalHistory4294967294;

makes std::vector::resize() request ~128 GiB on a 64-bit host, which
throws std::bad_alloc.  The exception propagates out of
slang_reflect_spirv() up to vulkan_filter_chain_create_from_preset()
with no try/catch in between, and the unhandled C++ exception
terminates RetroArch.  Reachable from any slang preset -- preset packs
are downloaded via the Online Updater and shipped third-party.

Severity is DoS rather than memory corruption, but the threat class
mirrors the recently-fixed .bsv replay file, .bps soft-patch, and
image-decoder bugs in 7335b37: a downloadable file that an end user
opens, leading to a guaranteed process termination.

* gfx/drivers_shader/slang_process.cpp

  Lift the existing PASS_OUTPUT bound check into a small helper
  validate_texture_semantic_index() and extend it to cover all
  arrayed semantics with their natural caps:

     PASS_OUTPUT      -> reflection->pass_number    (unchanged)
     PASS_FEEDBACK    -> GFX_MAX_SHADERS            (64)
     ORIGINAL_HISTORY -> GFX_MAX_FRAME_HISTORY      (128)
     USER             -> GFX_MAX_TEXTURES           (64)

  Non-arrayed semantics (Original, Source) carry index 0 by
  construction in slang_name_to_texture_semantic_array() and
  accept unconditionally.

  PASS_OUTPUT keeps its dedicated "Non causal filter chain
  detected" log verbiage so existing shader-author muscle memory
  and log-grepping scripts continue to work; the other semantics
  use a uniform "Texture semantic <name> index #N exceeds bound
  (<cap_label> = <cap>)" message that names both the offending
  index and the cap that rejected it.

  The helper is called from both call sites that lead into a
  resize_minimum():
    - add_active_buffer_ranges() (UBO uniform path), replacing
      the inline PASS_OUTPUT-only check.
    - slang_reflect()'s direct sampler-binding loop (the SLANG_
      INVALID_TEXTURE_SEMANTIC short-circuit is preserved).

* samples/gfx/slang_texture_index_bounds/,
  .github/workflows/Linux-samples-gfx.yml

  Regression test following the same convention as the v3 vulkan/
  tests in this directory and the security regression tests under
  samples/tasks/.  Keeps a verbatim copy of
  validate_texture_semantic_index() demarcated by `=== verbatim
  copy ===` markers, with an `IMPORTANT: ... must follow` comment.
  Plain C, asserts manually, exits nonzero on failure.

  21 cases across six probe groups: legitimate-in-bounds across
  all four arrayed semantics, at-cap rejections, the smoking-gun
  UINT32_MAX-1 indices on each path including the previously-
  unbounded ORIGINAL_HISTORY/PASS_FEEDBACK/USER ones, log-dispatch
  consistency, non-arrayed (ORIGINAL/SOURCE) accept-everything,
  and the PASS_OUTPUT-in-pass-0 reject-everything case.  Verified
  pre/post-patch discrimination: under the pre-fix
  PASS_OUTPUT-only shape the 7 cases that exercise the new bounds
  fail (3 boundary + 3 smoking-gun + 1 ORIGINAL_HISTORY at cap);
  PASS_OUTPUT cases continue to pass.

  Wires into the existing Linux-samples-gfx.yml workflow as a
  fourth step, modeled on the three vulkan/ steps from da5e650
  with the same per-step explanation of what the test is
  regression-testing and the "verbatim copy must follow" reminder.
2026-04-28 14:41:22 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre da5e650ccf gfx/vulkan: fix three heap-corruption bugs in driver and common layer + CI
Three independent issues found during a focused audit of gfx/drivers/
vulkan.c and gfx/common/vulkan_common.c, all in the same memory-safety
class (writes past compile-time-sized arrays or undersized
allocations).  Submitting together because they share the same audit
context and none stands alone particularly well.  Adds AddressSanitizer
regression tests for each under samples/gfx/, wired into a new
.github/workflows/Linux-samples-gfx.yml CI job.

* vulkan_find_device_extensions: heap overflow
  (gfx/common/vulkan_common.c)

  The required-extension list is currently written twice -- once via
  memcpy at the top of the body, then again in a per-element loop
  that appears to be left over from an earlier refactor.  The
  instance-side counterpart vulkan_find_instance_extensions only
  logs in its loop, confirming the divergence.  The redundant write
  consumes more slots in the caller's `enabled` buffer than the slot
  accounting expects.

  Inside vulkan_context_create_device_wrapper the buffer is sized
  for (info.enabledExtensionCount + ARRAY_SIZE(required) +
  ARRAY_SIZE(optional)) entries, but the function actually writes
  (enabledExtensionCount + 2*required + optional) entries
  worst-case.  With current values (1 required, 3 optional) that is
  a one-element heap overflow at the end of the malloc'd block
  whenever a libretro core uses the Vulkan HW context-negotiation
  interface and the GPU supports at least one optional extension.
  The other call site in vulkan_context_init_device uses a stack
  buffer of size 8 and stays inside it today, but only by a margin
  of three.

  The duplicate writes also produce duplicate entries in
  ppEnabledExtensionNames passed to vkCreateDevice, which the spec
  forbids (VUID-VkDeviceCreateInfo-ppEnabledExtensionNames-01840);
  strict validation layers will have been complaining.

  Drop the per-element loop -- the early vulkan_find_extensions()
  check at the top of the function already validates that all
  required extensions are present, so the per-element re-check has
  never done any work.  Keep the debug log.

* vulkan_create_swapchain: OOB writes from unclamped image count
  (gfx/common/vulkan_common.c)

  vulkan_create_swapchain calls vkGetSwapchainImagesKHR twice --
  first to query the count, then to fill swapchain_images[] -- and
  never clamps the count between the two calls.  swapchain_images,
  swapchain_fences, the four swapchain_*_semaphores arrays,
  readback.staging[] and vk->swapchain[] are all sized at compile
  time to VULKAN_MAX_SWAPCHAIN_IMAGES (8).  If a driver returns
  more images than that the second vkGetSwapchainImagesKHR writes
  past swapchain_images[], and every loop bounded by
  num_swapchain_images (~12 sites across init/deinit/textures/
  buffers/descriptor pools/command buffers/readback and direct
  vk->swapchain[i] accesses) walks past its array.

  Two clamps.  First, cap desired_swapchain_images to
  VULKAN_MAX_SWAPCHAIN_IMAGES before vkCreateSwapchainKHR so we
  never ask a well-behaved driver for more images than we can
  hold.  Second, clamp num_swapchain_images between the two
  vkGetSwapchainImagesKHR calls to handle drivers that return
  more images than were requested -- the spec permits this.

* vulkan_create_texture: 32-bit overflow in staging-buffer sizing
  (gfx/drivers/vulkan.c)

  buffer_width = width * bpp and buffer_info.size = buffer_width *
  height are both unsigned * unsigned in 32-bit before being
  widened to VkDeviceSize on assignment.  With dimensions large
  enough to wrap (e.g. width=65536, height=16385, bpp=4 ->
  0x1_0004_0000 wraps to 0x40000), the staging buffer is allocated
  at the wrapped (small) size while the per-row upload memcpy loop
  later in the same function walks the unmodified width x height.
  The mapped region is smaller than what the loop writes, so the
  memcpy walks past the mapping into adjacent heap memory.

  Reachable from libretro cores supplying oversized
  retro_framebuffer dimensions and from vulkan_load_texture /
  vulkan_set_texture_frame, which take dimensions originating in
  image decoders.

  Compute the staging-buffer size in 64-bit, and widen the upload
  loop's stride and per-row copy size to size_t.  VkDeviceSize is
  already 64-bit so the assignment to buffer_info.size is now
  correct end-to-end.  The loop's pointer math was already
  implicitly widened on 64-bit hosts but is now unconditionally
  correct on 32-bit hosts (3DS, Vita, PSP, Wii, Wii U, older
  Android, 32-bit Windows) as well.

* samples/gfx/, .github/workflows/Linux-samples-gfx.yml

  Three regression tests, one per fix above, following the pattern
  established by the security regression tests under samples/tasks/
  (archive_name_safety_test, http_method_match_test,
  video_shader_wildcard_test, input_remap_bounds_test,
  bsv_replay_bounds_test, bps_patch_bounds_test).  Each test keeps
  a verbatim copy of the relevant post-fix predicate, demarcated by
  `=== verbatim copy ===` markers; an `IMPORTANT: ... must follow`
  comment documents the convention.  Plain C, asserts manually,
  exits nonzero on failure -- matches the harness the existing
  Linux-samples-{tasks,libretro-{common,db}}-samples.yml workflows
  understand.

  vulkan_extension_count_test (5 cases): create_device_wrapper-
  shaped caller, init_device-shaped caller, GPU supporting no
  optional extensions, GPU missing a required extension, and the
  prepushed-via-negotiation-interface case.  Verified pre/post-
  patch discrimination: under the pre-fix double-write shape the
  create_device_wrapper case ASan-aborts with heap-buffer-overflow.

  vulkan_swapchain_clamp_test (5 cases): well-behaved 3-image
  driver, at-capacity boundary, 9-image (MAX+1) driver,
  pathological 64-image driver, and the request-side cap across
  six input values.  Verified pre/post-patch discrimination: under
  the pre-fix no-clamp shape the 9-image case ASan-aborts on the
  second vkGetSwapchainImagesKHR fill.

  vulkan_texture_size_test (6 cases): typical 320x240, 4K RGBA,
  the smoking-gun 65536x16385x4 wrap case, an assorted-shapes
  table, the per-row upload-loop strides under an ASan-instrumented
  buffer, and a 64-bit-result sanity check.  Verified pre/post-
  patch discrimination: under the pre-fix 32-bit arithmetic the
  overflow cases produce arithmetic mismatches against the 64-bit
  reference.

  Linux-samples-gfx.yml is a near-verbatim copy of
  Linux-samples-tasks.yml, runs each test under
  -fsanitize=address with a per-step explanation of what the test
  is regression-testing and the `verbatim copy must follow`
  reminder.  ~3 seconds per test on Ubuntu 24 with system gcc.
2026-04-28 14:13:31 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre c41e955464 tasks/task_patch: bound BPS patch action-loop and size-prelude against attacker-controlled inputs
bps_apply_patch (tasks/task_patch.c) implemented BPS patch
application without bounds checks on any of the per-command
reads or writes.  The action loop:

  while (bps.modify_offset < bps.modify_length - 12)
  {
     size_t _len = bps_decode(&bps);    /* attacker-chosen */
     unsigned mode = _len & 3;
     _len = (_len >> 2) + 1;

     switch (mode)
     {
        case TARGET_READ:
           while (_len--) {
              uint8_t data = bps_read(&bps);
              bps.target_data[bps.output_offset++] = data;   /* OOB write */
           }
        case SOURCE_READ:
           while (_len--) {
              uint8_t data = bps.source_data[bps.output_offset];   /* OOB read */
              bps.target_data[bps.output_offset++] = data;
           }
        case SOURCE_COPY:
           bps.source_offset += offset;       /* unbounded signed offset */
           while (_len--) {
              uint8_t data = bps.source_data[bps.source_offset++];   /* OOB read */
              bps.target_data[bps.output_offset++] = data;
           }
        case TARGET_COPY:
           bps.target_offset += offset;
           while (_len--) {
              uint8_t data = bps.target_data[bps.target_offset++];
              bps.target_data[bps.output_offset++] = data;
           }
     }
  }

A malicious .bps could:
  - Write attacker-chosen bytes far past the malloc'd
    target_data buffer (heap-buffer-overflow WRITE) via any
    of the four modes.
  - Read past source_data via SOURCE_COPY's unbounded signed
    offset.  Negative offsets underflow source_offset to a
    huge size_t; positive offsets push it past
    source_length.  The leaked bytes flow into target_data
    and the patch checksum, exposing heap contents.
  - Read past modify_data via TARGET_READ when _len exceeds
    the remaining patch bytes.  The action-loop guard
    confirms only "at least one command's worth of bytes",
    not the full read range.

The size prelude (modify_source_size / modify_target_size /
modify_markup_size, decoded by bps_decode immediately after
the "BPS1" header) was also unchecked:

  - modify_target_size declared as size_t silently truncated
    the uint64 bps_decode result on 32-bit hosts.  A value of
    0x100000000 wraps to 0; the subsequent malloc(0) returns
    a tiny allocation while bps.target_length is set to the
    raw uint64.  The next target_data[output_offset++] write
    OOB by the truncated delta.
  - modify_markup_size was used as a loop bound consuming
    bytes from modify_data via bps_read.  A value above the
    remaining patch bytes ran the read loop off the end of
    modify_data.

Reachability: user has soft-patching enabled (the default
when a .bps file with the same basename as the loaded ROM is
present) and loads a ROM whose .bps was supplied by an
attacker.  Same threat class as CDFS / CHD / BSV file-load
bugs.

Fix:

  1. Capture the three size-prelude values as raw uint64,
     reject anything above SIZE_MAX (so the size_t cast is
     guaranteed lossless), and reject markup_size that would
     drive bps_read past modify_length.

  2. At the top of every action-loop iteration, bound _len
     against (target_length - output_offset).  This single
     bound covers the target_data write in all four modes.

  3. In each mode, bound the source-side read range:
     - SOURCE_READ: output_offset + _len <= source_length
     - TARGET_READ: modify_offset + _len <= modify_length - 12
     - SOURCE_COPY: source_offset (post-offset-add) + _len
                    <= source_length
     - TARGET_COPY: target_offset (post-offset-add) + _len
                    <= target_length

Adds samples/tasks/bps_patch/bps_patch_bounds_test as a
regression test, registered in
.github/workflows/Linux-samples-tasks.yml as an ASan-enabled
step.  Five cases covering OOB-write rejection, OOB-read
rejection, legitimate operations, and the exact-capacity
boundary.

Test follows the existing samples/tasks pattern (verbatim
copy of the post-fix predicates with maintenance-contract
comment, ASan as the discriminator).  Verified to fire
under ASan when the bounds are removed: heap-buffer-overflow
WRITE of size 1 at offset 4 of a 4-byte allocation in
apply_action_loop.  With the bounds the test passes clean.
2026-04-28 06:08:15 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre 003a1ff74b input/bsv: bound key_event_count and input_event_count against array size
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.
2026-04-28 03:47:05 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre cba3a9c909 input: bound input_remap_ids[] against analog_value[][8] OOB write
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.
2026-04-28 03:26:06 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre bac68947b0 gfx/video_shader_parse: clamp replace_text length and prefix+_len in wildcard replacement
video_shader_replace_wildcards_impl built each replacement
value into a 256-byte stack buffer:

  char replace_text[256];
  _len = strlcpy(replace_text, source, sizeof(replace_text));

then unbounded:

  memcpy(dst + prefix, replace_text, _len);
  strlcpy(dst + prefix + _len, found + token_len,
          PATH_MAX_LENGTH - prefix - _len);

Two related primitives were exposed:

 1. strlcpy returns strlen(source) regardless of destination
    capacity.  When source resolved to a $CONTENT-DIR$ /
    $PRESET-DIR$ / $CORE$ / $GAME$ value longer than 256
    bytes (DIR_MAX_LENGTH is 4096, library_name and
    path_basename are platform-dependent and routinely
    exceed 256), _len was strlen(source).  The memcpy then
    read past the end of the 256-byte replace_text array
    into adjacent stack memory.

 2. The same _len fed (size_t)(PATH_MAX_LENGTH - prefix -
    _len) on the next line.  When prefix + _len exceeded
    PATH_MAX_LENGTH the subtraction underflowed size_t to a
    huge value and the strlcpy walked off the end of dst.
    Even with the _len clamp from (1), a wildcard token
    placed near the end of the input path (legitimate for
    an attacker-supplied .slangp/.glslp) lets prefix +
    REPLACE_BUF_SZ exceed PATH_MAX_LENGTH on its own.

The snprintf-chained branches (CORE_REQUESTED_ROTATION,
VIDEO_USER_ROTATION, VIDEO_FINAL_ROTATION,
SCREEN_ORIENTATION) had a sibling problem: snprintf returns
the would-have-written length, so _len += snprintf(...) on
a saturated buffer pushes _len past sizeof(replace_text)
even when the destination ate the entire size budget.

Reachable via shader presets the user opens.  These come
from the online updater (typically slang-shaders or
common-shaders), the user's own preset directory, and from
content-bundled presets distributed alongside ROMs.

Fix: clamp at the use site so all the upstream branches are
covered in one place.

  if (_len >= sizeof(replace_text))
     _len = sizeof(replace_text) - 1;
  if (prefix >= PATH_MAX_LENGTH || _len >= PATH_MAX_LENGTH - prefix)
     break;

Adds samples/tasks/video_shader_parse/video_shader_wildcard_test
as a regression test, registered in
.github/workflows/Linux-samples-tasks.yml as an ASan-enabled
step.  Four cases:

  - short replacement produces correct output
  - long source (600 chars) clamped without OOB read
  - huge source (4000 chars) clamped without strlcpy underflow
  - prefix near PATH_MAX_LENGTH correctly bails out

Test follows the existing samples/tasks pattern (verbatim
copy of the post-fix arithmetic block, ASan as the
discriminator) used by archive_name_safety_test and
http_method_match_test.  Verified to fire under ASan when
the clamps are removed: stack-buffer-overflow READ of size
600 at offset 288 in replace_text (a 256-byte buffer).
With the clamps the test passes clean.

Note on test scope: the test exercises a verbatim copy of
the post-replacement arithmetic block from
video_shader_parse.c lines 344-358, not the full
video_shader_replace_wildcards_impl.  The full function
depends on settings_t / runloop_state / video driver
globals that aren't tractable to stub for a unit test --
following the convention used by archive_name_safety_test
("if task_decompress.c amends the predicate, the copy here
must follow"), the test's verbatim block must track the
production block.  A comment in the test points at the
production lines explicitly.
2026-04-28 02:48:49 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre ae3292fe03 task_http: fix heap-buffer-overflow in GET fast-path + regression test
task_push_http_transfer_generic() decided whether a request is a "plain"
GET (and therefore subject to dedup via task_http_finder) by doing:

    method = net_http_connection_method(conn);
    if (memcmp(method, "GET", 3) == 0 && method[3] == '\0')

`method` is a pointer into a buffer that net_http_connection_new()
strdup'd from caller-supplied data; its allocation is exactly
strlen(method) + 1 bytes.  Two cases overrun that buffer:

1) method == NULL.  task_push_http_transfer_with_content() forwards the
   caller-supplied `method` argument straight to net_http_connection_new()
   without a NULL check; net_http_connection_new() itself permits a NULL
   method (conn->method stays NULL).  net_http_connection_method() then
   returns NULL and the memcmp dereferences it.

2) method shorter than 3 chars (e.g. "" or a 1-char custom verb).
   strdup allocates 1 or 2 bytes; memcmp reads 3 bytes regardless,
   overrunning the heap allocation.  AddressSanitizer reports:

     ==N==ERROR: AddressSanitizer: heap-buffer-overflow
     READ of size 3 at <addr> thread T0
       #0 ... memcmp
       #1 task_push_http_transfer_generic tasks/task_http.c:274
     <addr> is located 0 bytes after 2-byte region [...,...]
     allocated by thread T0 here:
       #0 ... strdup
       #1 net_http_connection_new

The trailing `method[3] == '\0'` guard happens *after* memcmp's read,
so it cannot prevent the overflow.

Replace the unsafe pair with string_is_equal(), which is bounded by the
buffer's own NUL terminator and is already used elsewhere in this file
(see task_http_finder at line 242 and the existing #include of
<string/stdstring.h>).  Add an explicit NULL guard for the method that
takes the existing error path; this also frees `conn` via the goto so
the existing ownership invariant on the error label is preserved.

Add a standalone regression test under samples/tasks/http/ and wire it
into Linux-samples-tasks CI.  The test mirrors
samples/tasks/decompress/archive_name_safety_test.c: it keeps a
verbatim copy of the predicate (the dispatch decision is local to
task_push_http_transfer_generic and not in a public header) and
exercises it against the truth-table plus the bounds-safety regression
cases ("", "X", "GE", NULL).  Inputs are strdup'd so each buffer's
allocation is exactly strlen+1, matching what net_http_connection_new()
does and making the pre-fix expression trip ASan in CI.

The Makefile honours SANITIZER= the same way samples/tasks/database/
does, and the new CI step builds with SANITIZER=address so a regression
to any unsafe expression is caught at the bounds level, not just the
truth-table level.  Verified locally: with the post-fix predicate the
test passes clean under ASan; reverting the predicate to the pre-fix
memcmp causes ASan to abort with "heap-buffer-overflow ... READ of
size 3" on the empty-string case.

build-essential on ubuntu-latest already pulls libasan via gcc-13's
libgcc-13-dev, so no apt-get changes are needed in the workflow.

Found via a focused ASan harness around the task_http.c finish-path
ownership logic.
2026-04-25 01:44:20 +02:00
U-DESKTOP-SPFP6AQ\twistedtechre 96ec45a82f samples/tasks: repair database demo, add decompress test
Two related changes under samples/tasks/.

samples/tasks/database: restore a buildable state

  The database demo had bit-rotted to the point where neither the
  C source nor the Makefile matched the current tree:

    * main_msg_queue_push: retro_task_queue_msg_t gained a
      retro_task_t* first parameter
    * main_db_cb: retro_task_callback_t gained the same
    * core_info_init_list: added a sixth bool *cache_supported
      parameter
    * Makefile: hash/rhash.c was renamed to hash/lrc_hash.c
    * Makefile: 7zip source list was stale -- 7zIn.c was renamed to
      7zArcIn.c, and BraIA64.c / CpuArch.c / Delta.c were added
    * Makefile: missing sources for trans_stream*, rzip_stream,
      features_cpu, rtime, rxml, m3u_file, logiqx_dat,
      manual_content_scan, yxml
    * link: missing -lm for roundf

  Several symbols reached via task_push_dbscan are defined only
  when RARCH_INTERNAL is set, which would transitively require
  linking most of retroarch.c, frontend_driver.c, runloop.c,
  configuration.c and the UI/video subsystems.  Since this sample
  exercises only the task queue, it provides lightweight stubs for:

    * msg_hash_get_help_us_enum  (real def in intl/msg_hash_us.c)
    * msg_hash_to_str_us         (real def in intl/msg_hash_us.c)
    * config_get_ptr             (real def in configuration.c)
    * runloop_msg_queue_push     (real def in runloop.c)
    * retroarch_override_setting_is_set
    * ui_companion_driver_notify_refresh
    * video_display_server_set_window_progress
    * frontend_driver_get_free_memory
    * dir_list_new_special       (real def in retroarch.c)

  All stubs are either no-ops or return a conservative value that
  lets the surrounding code bail gracefully; none of them are
  exercised on the happy dbscan path.

  After these changes the demo builds cleanly on a stock Ubuntu
  host (build-essential, zlib1g-dev, libpthread), and both
  `database_task` with no args and `database_task <5 dirs>` exit
  with the expected status.

samples/tasks/decompress: new standalone regression test

  Adds archive_name_safety_test, a 23-case predicate unit test for
  the archive_name_is_safe() function in tasks/task_decompress.c.

  This test was originally developed alongside the Zip Slip patch
  in the first security round and held out of
  libretro-common/samples/ because it exercises code in tasks/
  rather than libretro-common/.  samples/tasks/ is its natural
  home.

  The test keeps a verbatim copy of the predicate and validates it
  against a table of safe / unsafe paths:

    * safe:   file.txt, dir/file.txt, a.b/c, ..foo, foo..
    * unsafe: .., ../etc/passwd, foo/..//bar, /etc/passwd,
              \windows\system32, C:\evil, c:/evil, ..\..\windows,
              foo\..\bar, empty string, NULL

  If the predicate in task_decompress.c is ever edited, the copy
  here must be updated to match -- this is noted in the test file's
  header.
2026-04-19 18:24:46 +02:00
jdgleaver c37f540b72 Core Info Cache Improvements
- Core info cache can now be enabled/disabled on all platforms via a new `Settings > Core > Cache Core Info Files` option
- Core info cache file has been renamed from `.cache` to `core_info.cache` (i.e. it is no longer a 'hidden' file on Unix platforms, so can be deleted easily)
- The core info cache file is now compressed (rzip) to further reduce disk IO
- The presence of a `core_info.refresh` file in the core info directory will force a one-time refresh of the info cache. This file is generated automatically when toggling on the `Cache Core Info Files` option, and we will also add it to core info file packaging such that updating info files (either manually or via the online updater) will force a refresh
- The core info cache no longer contains 'core is locked' and 'firmware missing' data fields; these are 'dynamic' properties that must be determined at runtime
- The 'core is locked' status is now determined on core info intialisation by parsing the core directory listing, rather than by performing indivdual 'lock file exists' checks. This minimises file IO, and greatly improves performance on devices with slow storage
- While parsing the core info cache file, we now avoid unnecessary `strdup()`s when adding entries to the resultant cache list
- Memory leaks (potential and real) have been fixed, and safety checks added
- Build errors have been fixed
2021-05-06 15:00:55 +01:00
schellingb ba1ed2da4b New faster json parser/writer library rjson
Replace existing libraries jsonsax_full and jsonsax with it
2020-10-12 03:02:20 +09:00
twinaphex 772ed15d88 Remove file_path_str from Salamander 2020-08-22 13:42:57 +02:00
twinaphex df8f93e9d5 Split up VFS file_path code into separate file 2020-01-09 15:44:48 +01:00
twinaphex 278dc75285 (tasks) Fix samples 2020-01-04 12:07:58 +01:00
Brad Parker 6154371692 use threads for command-line scanner if available 2019-05-21 22:53:36 -04:00
Brad Parker f9a8c617ce add pthread for command-line scanner when platform is not windows 2019-05-21 22:41:17 -04:00
twinaphex 6c1310ca06 (database task standalone) Add HAVE_THREADS 2019-05-22 04:28:11 +02:00
Brad Parker 388a931f95 improve Makefile for standalone scanner 2019-05-21 18:33:07 -04:00
twinaphex 98b7a21c42 (task database standalone) Add 7zip and zlib support 2019-05-22 00:18:25 +02:00
twinaphex 1a1418ccfb (Samples) tasks/database - should work again 2019-05-21 22:38:50 +02:00
orbea 28ff4b391a Clean up white space. 2019-02-03 16:00:50 -08:00
twinaphex 5697a24946 (samples/tasks/database) Define -DHAVE_COMPRESSION 2018-04-10 05:28:58 +02:00
twinaphex 25ab35b7d0 (samples/tasks/database) Add hardcoded extensions for OSX 2018-04-10 05:26:47 +02:00
twinaphex 639152e291 Hardcode exts for now - we assume this standalone program will
only be used on Windows/OSX/Linux systems for now
2018-04-10 05:20:19 +02:00
twinaphex c24179dc63 Scanning should now work in standalone program 2018-04-10 05:16:33 +02:00
twinaphex d66b8fa882 Start implementing main function 2018-04-10 04:53:12 +02:00
twinaphex 00cc4ca7aa (samples) Gets close to linking now 2018-04-10 03:53:46 +02:00
twinaphex 7ccecc80c1 Update samples/tasks/database 2018-04-10 02:43:35 +02:00
twinaphex 908de15e7b Start adding samples - not done yet 2018-04-09 22:12:59 +02:00