fix: cap justify fill array at 256 chars to shrink ~1.25 MB stack frame (#818)

co_center/co_ljust/co_rjust sized fill_char_t fchars[LBUF_SIZE] — one
entry per LBUF byte — at ~40 bytes each, a ~1.25 MB stack frame. On
Windows's 1 MB default EXE stack reserve, any center()/ljust()/rjust()
call overflowed the stack and killed netmux instantly (0xc00000fd);
worked around in 27cc17c39 by raising the reserve to 8 MB.

The array holds the *fill pattern* (the cyclic pad argument), which is
almost always a handful of characters, so the LBUF sizing was pure
waste. Cap it at CO_FILL_CHARS_MAX (256); parse_fill_chars already
honors its max_chars argument, so a longer fill simply repeats from the
start at that boundary. co_center's frame drops from ~1.31 MB to ~42 KB
(measured: add $0xa878,%rsp), the bulk now the legitimate 32 KB
fill_buf.

A stack array (not heap or static) is required: color_ops.c is compiled
into the freestanding rv64 JIT blob, which has no malloc and no .bss.
co_reverse's elems[LBUF_SIZE] (512 KB) is left as-is — it is sized to
the input string being reversed (a genuine worst case), not a fill
pattern, and 512 KB stays under the 1 MB reserve.

Regression coverage: center_fn.mux TC007 runs center/ljust/rjust with a
400-character (over-cap) fill and asserts correct output width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stephen Dennis 2026-06-12 21:48:09 -06:00
parent 79fb983903
commit a7a816ea8e
6 changed files with 88 additions and 43 deletions

Binary file not shown.

View file

@ -3464,6 +3464,18 @@ typedef struct {
co_ColorState color; /* color state at this position */
} fill_char_t;
/*
* Maximum distinct fill characters parsed from a fill pattern. The fill
* is the cyclic pad argument to center()/ljust()/rjust(), almost always a
* handful of characters, so one fill_char_t per LBUF byte was a ~1.25 MB
* stack frame for nothing (#818 instant stack overflow on Windows's
* 1 MB default reserve). A pattern with more than this many visible
* characters repeats from the start at this boundary. color_ops.c is
* compiled into the freestanding rv64 blob, so this must be a stack
* array, not a heap or static allocation.
*/
#define CO_FILL_CHARS_MAX 256
/*
* parse_fill_chars Pre-process fill pattern into per-character colors.
*
@ -3656,9 +3668,9 @@ size_t co_center(unsigned char *out,
}
/* Parse fill into per-character colors. */
fill_char_t fchars[LBUF_SIZE];
fill_char_t fchars[CO_FILL_CHARS_MAX];
size_t fill_width = 0;
size_t nfchars = parse_fill_chars(fchars, LBUF_SIZE, fill_buf, flen,
size_t nfchars = parse_fill_chars(fchars, CO_FILL_CHARS_MAX, fill_buf, flen,
&fill_width);
if (fill_width == 0) {
/* Default to space fill. */
@ -3731,9 +3743,9 @@ size_t co_ljust(unsigned char *out,
}
/* Parse fill into per-character colors. */
fill_char_t fchars[LBUF_SIZE];
fill_char_t fchars[CO_FILL_CHARS_MAX];
size_t fill_width = 0;
size_t nfchars = parse_fill_chars(fchars, LBUF_SIZE, fill_buf, flen,
size_t nfchars = parse_fill_chars(fchars, CO_FILL_CHARS_MAX, fill_buf, flen,
&fill_width);
if (fill_width == 0) {
fchars[0].bytes[0] = ' ';
@ -3797,9 +3809,9 @@ size_t co_rjust(unsigned char *out,
}
/* Parse fill into per-character colors. */
fill_char_t fchars[LBUF_SIZE];
fill_char_t fchars[CO_FILL_CHARS_MAX];
size_t fill_width = 0;
size_t nfchars = parse_fill_chars(fchars, LBUF_SIZE, fill_buf, flen,
size_t nfchars = parse_fill_chars(fchars, CO_FILL_CHARS_MAX, fill_buf, flen,
&fill_width);
if (fill_width == 0) {
fchars[0].bytes[0] = ' ';
@ -5382,13 +5394,13 @@ unsigned char co_dfa_ascii(const unsigned char *p)
/* ---- co_render_ascii ---- */
#line 5175 "color_ops.c"
#line 5187 "color_ops.c"
static const int render_ascii_start = 12;
static const int render_ascii_en_main = 12;
#line 3915 "color_ops.rl"
#line 3927 "color_ops.rl"
size_t co_render_ascii(unsigned char *out,
@ -5402,21 +5414,21 @@ size_t co_render_ascii(unsigned char *out,
const unsigned char *wp_end = out + LBUF_SIZE - 1;
#line 5191 "color_ops.c"
#line 5203 "color_ops.c"
{
cs = render_ascii_start;
}
#line 3928 "color_ops.rl"
#line 3940 "color_ops.rl"
#line 5194 "color_ops.c"
#line 5206 "color_ops.c"
{
if ( p == pe )
goto _test_eof;
switch ( cs )
{
tr0:
#line 3900 "color_ops.rl"
#line 3912 "color_ops.rl"
{
/* Run visible code point through tr_ascii DFA for approximation. */
if (*mark < 0x80) {
@ -5430,9 +5442,9 @@ tr0:
}
goto st12;
tr7:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
#line 3900 "color_ops.rl"
#line 3912 "color_ops.rl"
{
/* Run visible code point through tr_ascii DFA for approximation. */
if (*mark < 0x80) {
@ -5449,7 +5461,7 @@ st12:
if ( ++p == pe )
goto _test_eof12;
case 12:
#line 5230 "color_ops.c"
#line 5242 "color_ops.c"
switch( (*p) ) {
case 0u: goto st0;
case 224u: goto tr9;
@ -5478,62 +5490,62 @@ st0:
cs = 0;
goto _out;
tr8:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st1;
st1:
if ( ++p == pe )
goto _test_eof1;
case 1:
#line 5264 "color_ops.c"
#line 5276 "color_ops.c"
if ( 128u <= (*p) && (*p) <= 191u )
goto tr0;
goto st0;
tr9:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st2;
st2:
if ( ++p == pe )
goto _test_eof2;
case 2:
#line 5274 "color_ops.c"
#line 5286 "color_ops.c"
if ( 160u <= (*p) && (*p) <= 191u )
goto st1;
goto st0;
tr10:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st3;
st3:
if ( ++p == pe )
goto _test_eof3;
case 3:
#line 5284 "color_ops.c"
#line 5296 "color_ops.c"
if ( 128u <= (*p) && (*p) <= 191u )
goto st1;
goto st0;
tr11:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st4;
st4:
if ( ++p == pe )
goto _test_eof4;
case 4:
#line 5294 "color_ops.c"
#line 5306 "color_ops.c"
if ( 128u <= (*p) && (*p) <= 159u )
goto st1;
goto st0;
tr12:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st5;
st5:
if ( ++p == pe )
goto _test_eof5;
case 5:
#line 5304 "color_ops.c"
#line 5316 "color_ops.c"
if ( (*p) < 148u ) {
if ( 128u <= (*p) && (*p) <= 147u )
goto st1;
@ -5551,38 +5563,38 @@ case 6:
goto st12;
goto st0;
tr13:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st7;
st7:
if ( ++p == pe )
goto _test_eof7;
case 7:
#line 5327 "color_ops.c"
#line 5339 "color_ops.c"
if ( 144u <= (*p) && (*p) <= 191u )
goto st3;
goto st0;
tr14:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st8;
st8:
if ( ++p == pe )
goto _test_eof8;
case 8:
#line 5337 "color_ops.c"
#line 5349 "color_ops.c"
if ( 128u <= (*p) && (*p) <= 191u )
goto st3;
goto st0;
tr15:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st9;
st9:
if ( ++p == pe )
goto _test_eof9;
case 9:
#line 5347 "color_ops.c"
#line 5359 "color_ops.c"
if ( (*p) < 176u ) {
if ( 128u <= (*p) && (*p) <= 175u )
goto st3;
@ -5600,14 +5612,14 @@ case 10:
goto st6;
goto st0;
tr16:
#line 3899 "color_ops.rl"
#line 3911 "color_ops.rl"
{ mark = p; }
goto st11;
st11:
if ( ++p == pe )
goto _test_eof11;
case 11:
#line 5370 "color_ops.c"
#line 5382 "color_ops.c"
if ( 128u <= (*p) && (*p) <= 143u )
goto st3;
goto st0;
@ -5629,7 +5641,7 @@ case 11:
_out: {}
}
#line 3929 "color_ops.rl"
#line 3941 "color_ops.rl"
*wp = '\0';
return (size_t)(wp - out);

View file

@ -1975,6 +1975,18 @@ typedef struct {
co_ColorState color; /* color state at this position */
} fill_char_t;
/*
* Maximum distinct fill characters parsed from a fill pattern. The fill
* is the cyclic pad argument to center()/ljust()/rjust(), almost always a
* handful of characters, so one fill_char_t per LBUF byte was a ~1.25 MB
* stack frame for nothing (#818 — instant stack overflow on Windows's
* 1 MB default reserve). A pattern with more than this many visible
* characters repeats from the start at this boundary. color_ops.c is
* compiled into the freestanding rv64 blob, so this must be a stack
* array, not a heap or static allocation.
*/
#define CO_FILL_CHARS_MAX 256
/*
* parse_fill_chars — Pre-process fill pattern into per-character colors.
*
@ -2167,9 +2179,9 @@ size_t co_center(unsigned char *out,
}
/* Parse fill into per-character colors. */
fill_char_t fchars[LBUF_SIZE];
fill_char_t fchars[CO_FILL_CHARS_MAX];
size_t fill_width = 0;
size_t nfchars = parse_fill_chars(fchars, LBUF_SIZE, fill_buf, flen,
size_t nfchars = parse_fill_chars(fchars, CO_FILL_CHARS_MAX, fill_buf, flen,
&fill_width);
if (fill_width == 0) {
/* Default to space fill. */
@ -2242,9 +2254,9 @@ size_t co_ljust(unsigned char *out,
}
/* Parse fill into per-character colors. */
fill_char_t fchars[LBUF_SIZE];
fill_char_t fchars[CO_FILL_CHARS_MAX];
size_t fill_width = 0;
size_t nfchars = parse_fill_chars(fchars, LBUF_SIZE, fill_buf, flen,
size_t nfchars = parse_fill_chars(fchars, CO_FILL_CHARS_MAX, fill_buf, flen,
&fill_width);
if (fill_width == 0) {
fchars[0].bytes[0] = ' ';
@ -2308,9 +2320,9 @@ size_t co_rjust(unsigned char *out,
}
/* Parse fill into per-character colors. */
fill_char_t fchars[LBUF_SIZE];
fill_char_t fchars[CO_FILL_CHARS_MAX];
size_t fill_width = 0;
size_t nfchars = parse_fill_chars(fchars, LBUF_SIZE, fill_buf, flen,
size_t nfchars = parse_fill_chars(fchars, CO_FILL_CHARS_MAX, fill_buf, flen,
&fill_width);
if (fill_width == 0) {
fchars[0].bytes[0] = ' ';

Binary file not shown.

View file

@ -113,11 +113,30 @@
eq(strlen(center(abcdef,xyz)), 0)
)=
{
@log smoke=TC006: center truncation. Succeeded.;
@log smoke=TC006: center truncation. Succeeded.
},
{
@log smoke=TC006: center truncation. Failed (t=[center(abcdef,3)] w0=[center(abcdef,0)] wx=[center(abcdef,xyz)]).
}
-
#
# Test Case #7 - Over-cap fill pattern. The fill array is capped at
# CO_FILL_CHARS_MAX (256) visible characters to avoid a ~1.25 MB stack
# frame (#818). A longer fill must still produce correct-width output
# without crashing - center/ljust/rjust here use a 400-character fill.
#
&tr.tc007 test_center_fn=
@if cand(
eq(vwidth(center(X,50,[repeat(ab,200)])), 50),
eq(vwidth(ljust(X,50,[repeat(ab,200)])), 50),
eq(vwidth(rjust(X,50,[repeat(ab,200)])), 50)
)=
{
@log smoke=TC007: justify over-cap fill width. Succeeded.;
@trig me/tr.done
},
{
@log smoke=TC006: center truncation. Failed (t=[center(abcdef,3)] w0=[center(abcdef,0)] wx=[center(abcdef,xyz)]).;
@log smoke=TC007: justify over-cap fill width. Failed (c=[vwidth(center(X,50,[repeat(ab,200)]))] l=[vwidth(ljust(X,50,[repeat(ab,200)]))] r=[vwidth(rjust(X,50,[repeat(ab,200)]))]).;
@trig me/tr.done
}
-

View file

@ -2017,7 +2017,9 @@
>262
"@if cand(strmatch(center(1,1,-+), 1),strmatch(center(X,5,-+), -+X+-),strmatch(center(X,7,-+), -+-X-+-))={@log smoke=TC005: Vary string and width with '-+' fill. Succeeded.},{@log smoke=TC005: Vary string and width with '-+' fill. Failed (c2=[center(X,5,-+)] c3=[center(X,7,-+)]).}"
>263
"@if cand(strmatch(center(abcdef,3), abc),eq(strlen(center(abcdef,0)), 0),eq(strlen(center(abcdef,xyz)), 0))={@log smoke=TC006: center truncation. Succeeded.;@trig me/tr.done},{@log smoke=TC006: center truncation. Failed (t=[center(abcdef,3)] w0=[center(abcdef,0)] wx=[center(abcdef,xyz)]).;@trig me/tr.done}"
"@if cand(strmatch(center(abcdef,3), abc),eq(strlen(center(abcdef,0)), 0),eq(strlen(center(abcdef,xyz)), 0))={@log smoke=TC006: center truncation. Succeeded.},{@log smoke=TC006: center truncation. Failed (t=[center(abcdef,3)] w0=[center(abcdef,0)] wx=[center(abcdef,xyz)]).}"
>264
"@if cand(eq(vwidth(center(X,50,[repeat(ab,200)])), 50),eq(vwidth(ljust(X,50,[repeat(ab,200)])), 50),eq(vwidth(rjust(X,50,[repeat(ab,200)])), 50))={@log smoke=TC007: justify over-cap fill width. Succeeded.;@trig me/tr.done},{@log smoke=TC007: justify over-cap fill width. Failed (c=[vwidth(center(X,50,[repeat(ab,200)]))] l=[vwidth(ljust(X,50,[repeat(ab,200)]))] r=[vwidth(rjust(X,50,[repeat(ab,200)]))]).;@trig me/tr.done}"
<
!35
"test_chaninfo_fn"