Skip to content

Commit 7f745a9

Browse files
committed
input: fix OOM handling in wl_read_pipe and rwebinput_keyboard_cb
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).
1 parent dc79a1e commit 7f745a9

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

input/common/wayland_common.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,27 @@ static ssize_t wl_read_pipe(int fd, void** buffer, size_t* total_length,
900900

901901
*buffer = output_buffer;
902902
}
903+
else
904+
{
905+
/* Allocation failed. Previously this branch silently
906+
* dropped the bytes_read data, left *total_length
907+
* incremented (so the caller thought the buffer had
908+
* grown), and returned a positive bytes_read - the
909+
* caller's 'while (wl_read_pipe(...) > 0)' loop then
910+
* continued and the next iteration wrote at offset
911+
* 'pos = *total_length' which sat past the end of the
912+
* still-unchanged *buffer, corrupting whatever lived
913+
* there. On realloc failure *buffer is also left
914+
* pointing at the old (smaller) allocation, so the
915+
* old data is still valid, but the length accounting
916+
* is a lie.
917+
*
918+
* Restore the invariant by rewinding *total_length
919+
* and reporting -1 to the caller so its while-loop
920+
* terminates. */
921+
*total_length = pos;
922+
bytes_read = -1;
923+
}
903924
}
904925
}
905926

input/drivers/rwebinput_input.c

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,33 @@ static EM_BOOL rwebinput_keyboard_cb(int event_type,
261261

262262
if (rwebinput->keyboard.count >= rwebinput->keyboard.max_size)
263263
{
264-
size_t new_max = MAX(1, rwebinput->keyboard.max_size << 1);
265-
rwebinput->keyboard.events = realloc(rwebinput->keyboard.events,
264+
size_t new_max = MAX(1, rwebinput->keyboard.max_size << 1);
265+
void *tmp = realloc(rwebinput->keyboard.events,
266266
new_max * sizeof(rwebinput->keyboard.events[0]));
267-
rwebinput->keyboard.max_size = new_max;
267+
/* Two bugs in the pre-patch form.
268+
*
269+
* (1) 'events = realloc(events, ...)' is the classic realloc-
270+
* assign-self leak: on OOM realloc returns NULL but the
271+
* original buffer is still valid; the self-assign
272+
* overwrites the only pointer to it, leaking the whole
273+
* pending-event ring.
274+
* (2) max_size was being updated unconditionally after the
275+
* realloc, before the return value was even inspected.
276+
* On OOM we then walked into the field writes below
277+
* which NULL-deref'd 'events' - but the recorded
278+
* max_size claimed the larger size, so any surviving
279+
* state said 'the buffer can hold 2N events' while
280+
* actually holding zero.
281+
*
282+
* On OOM drop this one event on the floor (the keyboard
283+
* event ring is bounded and the loss of a single input
284+
* event in a memory-starved system is survivable; we were
285+
* going to crash previously). Both max_size and events
286+
* stay at their pre-realloc values. */
287+
if (!tmp)
288+
return EM_TRUE;
289+
rwebinput->keyboard.events = tmp;
290+
rwebinput->keyboard.max_size = new_max;
268291
}
269292

270293
rwebinput->keyboard.events[rwebinput->keyboard.count].type = event_type;

0 commit comments

Comments
 (0)