Skip to content

unexpand: avoid heap overflow on negative-width blank characters - #361

Open
isl-Ramzi wants to merge 1 commit into
coreutils:masterfrom
isl-Ramzi:unexpand-neg-width-overflow
Open

isl-Ramzi wants to merge 1 commit into
coreutils:masterfrom
isl-Ramzi:unexpand-neg-width-overflow

Conversation

@isl-Ramzi

Copy link
Copy Markdown
Contributor

In unexpand(), the blank-accumulation path runs column += c32width (g.ch) without clamping, unlike the non-blank path just below it and the matching code in expand and fold, which treat a negative width as 1. A separator whose display width is negative never advances column, so the column >= next_tab_column flush never fires and the fixed pending_blank buffer (sized max_column_width * MB_CUR_MAX, on the assumption that every pending blank advances column by at least one) grows past its end. It is reachable from input in a multi-byte locale on platforms whose c32issep() accepts such a character as a blank, for example U+0085 (NEL), which iswspace() reports as a space on musl while wcwidth() returns -1; on glibc c32issep() is iswblank(), so only TAB and SPACE reach this path and the bug stays latent there. The 9.12 fix for this same buffer covered oversized tab values and over-long multi-byte blanks, but not a negative-width blank. Clamping the negative width to 1 matches the sibling sites and restores the one-column-per-blank invariant the buffer size relies on.

Verified under ASAN: feeding leading U+0085 characters (with the musl classification) writes two bytes past the 32-byte pending_blank region at src/unexpand.c:210 before this change, and runs clean afterwards.

* src/unexpand.c (unexpand): Clamp a negative c32width() to 1 when
accumulating a blank, matching the sibling non-blank path and expand
and fold.  A separator whose display width is negative (e.g. U+0085
where c32issep() accepts it) left 'column' un-advanced, so the pending
blank buffer, sized for one byte of advance per blank, grew without
bound.
* NEWS: Mention the bug fix.
@pixelb

pixelb commented Oct 2, 2026

Copy link
Copy Markdown
Member

I'll squash in a test...

diff --git a/tests/unexpand/mb.sh b/tests/unexpand/mb.sh
index 84ba0354e..dba7e00c9 100755
--- a/tests/unexpand/mb.sh
+++ b/tests/unexpand/mb.sh
@@ -178,4 +178,12 @@ ideo_space=$(env printf '\u3000')
   unexpand -t1 >out 2>err; ret=$?
 test "$ret" = 0 || { cat err; fail=1; }
 
+# On some platforms (musl) U+0085 is a blank with a negative display width.
+# Such blanks must advance the column, otherwise the pending-blank
+# buffer grows without bound.  Elsewhere they pass through unchanged.
+next_line=$(env printf '\u0085')
+{ yes "$next_line" | head -n 40000 | tr -d '\n'; echo; } |
+  unexpand -t1 >out 2>err; ret=$?
+test "$ret" = 0 || { cat err; fail=1; }
+
 Exit $fail

@collinfunk

Copy link
Copy Markdown
Member

Thanks for the report and patch @isl-Ramzi!

@pixelb I'm curious, were you able to reproduce it on glibc? I haven't been able to test it yet.

I guess we should probably take another look at the other c32width calls. It's always possible for places to have strange locale definitions.

@pixelb

pixelb commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

No it's only with the combo of c32issep() allowing 0x0085 through (on non glibc).
Actually another way to avoid the issue is adding 0x0085 to c32isvertspace() in system.h
I'll add a follow up commit to do that

@pixelb

pixelb commented Oct 2, 2026

Copy link
Copy Markdown
Member

I guess we should probably take another look at the other c32width calls. It's always possible for places to have strange locale definitions.

I did a quick audit there, and they seem OK.
Actually the column -= last_character_width in fold.c looks wrong as last_character_width can be > 1
I don't think that's a security issue, but I'll do a fix for that also

@collinfunk

Copy link
Copy Markdown
Member

No it's only with the combo of c32issep() allowing 0x0085 through (on non glibc). Actually another way to avoid the issue is adding 0x0085 to c32isvertspace() in system.h I'll add a follow up commit to do that

Nice, thanks for looking into that and the fold fix.

Perhaps it is worth mentioning that it can't happen on glibc in the NEWS entry? If it can be done without making it too wordy, that is. Maybe s/platforms/non-glibc &/?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants