menu: Fix EXC_BREAKPOINT when typing in OSK on macOS (in-place word_wrap)

ozone_draw_osk and the OSK caret rendering in xmb/materialui all
called word_wrap() with the same buffer as source and destination:

   (ozone->word_wrap)(cursor_message, sizeof(cursor_message),
         cursor_message, strlen(cursor_message), ...);

word_wrap()/word_wrap_wideglyph() copy via strlcpy(), whose buffers
must not overlap. On macOS, compat/strl.h force-defines HAVE_STRL, so
the system strlcpy is used; with _FORTIFY_SOURCE this resolves to
__strlcpy_chk, which aborts on any overlap (including exact aliasing)
via __chk_fail_overlap. As a result, typing a single character into
any OSK field (e.g. the IP Address setting under SMB Network
Settings) hits word_wrap's short-string early return
`strlcpy(s, src, len)` with s == src and raises EXC_BREAKPOINT.
Crash frame: len=2048, src_len=1, line_width=84, s == src == "1";
video_thread_loop -> ozone_draw_osk -> word_wrap -> __strlcpy_chk ->
__chk_fail_overlap. Linux/Windows never crashed only because the
strlcpy_retro__ fallback tolerates exact aliasing by accident.

In addition, in-place use of word_wrap_wideglyph() silently corrupts
CJK OSK text on all platforms: the lastwideglyph wrap branch
overwrites a source byte with '\n' before rewinding src onto it.
In-place wrapping of the kana string
E3 81 82 .. E3 81 93 (10 glyphs, line_width 4, wideglyph_width 200)
produces glyph 1 + 24 bare newlines + glyph 10 instead of one glyph
per line.

Fix: stage the OSK text in a separate cursor_src buffer at all three
call sites and wrap into the destination, matching the tmpstr pattern
already used for ozone's sidebar metadata. Document the
non-overlapping-buffers requirement for word_wrap()/
word_wrap_wideglyph() in stdstring.{c,h}. All other in-tree callers
(rgui, gfx_widgets, message boxes, sublabels) already use distinct
buffers.

Sanitizer sweep (ASan+UBSan+LSan, gcc, system strlcpy via
-DHAVE_STRL on glibc 2.39):
- fixed caller pattern yields "1" for the crashing input, clean
- separate-buffer output verified byte-exact against the previous
  in-place output for ASCII inputs across line widths 5/10/30/84
  (no rendering change for existing users)
- libretro-common word_wrap_wideglyph overflow regression tests pass
- ozone.c, xmb.c, materialui.c, stdstring.c compile clean
TSan: not applicable (single-threaded string path, no concurrency
change).
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-08-10 16:59:40 +02:00
1 parent 703133d989
commit bd79aee4a7
5 files changed
+50 -20

No files matched your search

@@ -203,6 +203,10 @@ char *string_trim_whitespace(char *const s);
* regular Latin characters - i.e. it will not wrap
* correctly any text containing so-called 'wide' Unicode
* characters (e.g. CJK languages, emojis, etc.).
* @dst and @src must NOT overlap: the copy is performed
* with strlcpy(), which has undefined behaviour for
* overlapping buffers (and aborts via __chk_fail_overlap
* with fortified libc on macOS).
**/
size_t word_wrap(char *dst, size_t dst_size, const char *src, size_t src_len,
int line_width, int wideglyph_width, unsigned max_lines);
@@ -240,6 +244,10 @@ size_t word_wrap(char *dst, size_t dst_size, const char *src, size_t src_len,
* if @src string contains 'wide' Unicode characters whose
* on-screen pixel width deviates greatly from the set
* @wideglyph_width value.
* @dst and @src must NOT overlap: the copy is performed
* with strlcpy(), which has undefined behaviour for
* overlapping buffers (and aborts via __chk_fail_overlap
* with fortified libc on macOS).
**/
size_t word_wrap_wideglyph(
char *dst, size_t dst_size,
+8
View File
@@ -207,6 +207,10 @@ char *string_trim_whitespace(char *const s)
* regular Latin characters - i.e. it will not wrap
* correctly any text containing so-called 'wide' Unicode
* characters (e.g. CJK languages, emojis, etc.).
* @s and @src must NOT overlap: the copy is performed
* with strlcpy(), which has undefined behaviour for
* overlapping buffers (and aborts via __chk_fail_overlap
* with fortified libc on macOS).
**/
size_t word_wrap(
char *s, size_t len,
@@ -317,6 +321,10 @@ size_t word_wrap(
* if @src string contains 'wide' Unicode characters whose
* on-screen pixel width deviates greatly from the set
* @wideglyph_width value.
* @s and @src must NOT overlap: the copy is performed
* with strlcpy(), which has undefined behaviour for
* overlapping buffers (and aborts via __chk_fail_overlap
* with fortified libc on macOS).
**/
size_t word_wrap_wideglyph(char *s, size_t len,
const char *src, size_t src_len, int line_width,
+11 -7
View File
@@ -2929,24 +2929,28 @@ static void materialui_render_messagebox(
if (input && line->buffer
&& ((menu_driver_get_current_time() / 500000) & 1))
{
char cursor_src[MENU_LABEL_MAX_LENGTH];
char cursor_msg[MENU_LABEL_MAX_LENGTH];
size_t len = (size_t)(input - msg + 1);
size_t ptr = line->ptr;
if (ptr > line->size)
ptr = line->size;
if (len < sizeof(cursor_msg))
if (len < sizeof(cursor_src))
{
if (ptr >= sizeof(cursor_msg) - len)
ptr = sizeof(cursor_msg) - len - 1;
if (ptr >= sizeof(cursor_src) - len)
ptr = sizeof(cursor_src) - len - 1;
memcpy(cursor_msg, msg, len);
memcpy(cursor_msg + len, line->buffer, ptr);
cursor_msg[len + ptr] = '\0';
/* word_wrap()/word_wrap_wideglyph() require
* non-overlapping source and destination
* buffers, so stage the source separately */
memcpy(cursor_src, msg, len);
memcpy(cursor_src + len, line->buffer, ptr);
cursor_src[len + ptr] = '\0';
(mui->word_wrap)(
cursor_msg, sizeof(cursor_msg),
cursor_msg, strlen(cursor_msg),
cursor_src, len + ptr,
usable_width / (int)mui->font_data.list.glyph_width,
mui->font_data.list.wideglyph_width, 0);
+12 -6
View File
@@ -7370,20 +7370,26 @@ OZONE_NOINLINE static void ozone_draw_osk(
if (!draw_placeholder && input_st->keyboard_line.buffer)
{
char cursor_src[2048];
char cursor_message[2048];
size_t ptr = input_st->keyboard_line.ptr;
if (ptr > input_st->keyboard_line.size)
ptr = input_st->keyboard_line.size;
if (ptr >= sizeof(cursor_message))
ptr = sizeof(cursor_message) - 1;
if (ptr >= sizeof(cursor_src))
ptr = sizeof(cursor_src) - 1;
memcpy(cursor_message, input_st->keyboard_line.buffer, ptr);
cursor_message[ptr] = '\0';
/* word_wrap()/word_wrap_wideglyph() require
* non-overlapping source and destination buffers
* (in-place use aborts under fortified strlcpy on
* macOS and corrupts wide glyphs in the wideglyph
* variant), so stage the source separately */
memcpy(cursor_src, input_st->keyboard_line.buffer, ptr);
cursor_src[ptr] = '\0';
(ozone->word_wrap)(cursor_message,
sizeof(cursor_message),
cursor_message,
strlen(cursor_message),
cursor_src,
ptr,
(video_width - (margin * 2) - (padding * 2)) / ozone->fonts.entries_label.glyph_width,
ozone->fonts.entries_label.wideglyph_width,
0);
+11 -7
View File
@@ -1355,24 +1355,28 @@ XMB_NOINLINE static void xmb_render_messagebox_internal(
if (input && line->buffer
&& ((menu_driver_get_current_time() / 500000) & 1))
{
char cursor_src[MENU_LABEL_MAX_LENGTH];
char cursor_message[MENU_LABEL_MAX_LENGTH];
size_t len = (size_t)(input - message + 1);
size_t ptr = line->ptr;
if (ptr > line->size)
ptr = line->size;
if (len < sizeof(cursor_message))
if (len < sizeof(cursor_src))
{
if (ptr >= sizeof(cursor_message) - len)
ptr = sizeof(cursor_message) - len - 1;
if (ptr >= sizeof(cursor_src) - len)
ptr = sizeof(cursor_src) - len - 1;
memcpy(cursor_message, message, len);
memcpy(cursor_message + len, line->buffer, ptr);
cursor_message[len + ptr] = '\0';
/* word_wrap()/word_wrap_wideglyph() require
* non-overlapping source and destination
* buffers, so stage the source separately */
memcpy(cursor_src, message, len);
memcpy(cursor_src + len, line->buffer, ptr);
cursor_src[len + ptr] = '\0';
(xmb->word_wrap)(
cursor_message, sizeof(cursor_message),
cursor_message, strlen(cursor_message),
cursor_src, len + ptr,
usable_width / (xmb->font_size * 0.85f),
xmb->wideglyph_width, 0);