diff options
| author | user <user@clank> | 2026-08-19 22:48:31 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-19 22:48:31 +0200 |
| commit | 009350acbcd4360680b8fa228d8cd03a2f2b5e93 (patch) | |
| tree | ef0eadb14cc8186829c4b7ce46955ea88443a405 | |
| parent | webdemo: the theme symlink is a dependency of the image (diff) | |
| download | slosh-009350acbcd4360680b8fa228d8cd03a2f2b5e93.tar.gz slosh-009350acbcd4360680b8fa228d8cd03a2f2b5e93.tar.xz slosh-009350acbcd4360680b8fa228d8cd03a2f2b5e93.zip | |
parsers: fix seven bugs found by fuzzing
kdl: heap-buffer-overflow on trailing backslash in a string (a"\)
jsonval: heap-buffer-overflow on truncated \uXXXX escape ("\u)
jsonval: UB float-to-long cast on out-of-range numbers (2e22)
expr: UB signed overflow in arithmetic ops — wrap as uint32
expr: INT32_MIN / -1 SIGFPE guard in OP_DIV/OP_MOD
expr: stack overflow on deep nesting — add MAX_DEPTH 256
expr: 64-bit + saturation in OP_DIST to prevent squared overflow
input: NULL memcpy in input_timeout's re-parse path
input: signed overflow in CSI and SGR mouse parameter accumulation
| -rw-r--r-- | src/expr.c | 54 | ||||
| -rw-r--r-- | src/input.c | 22 | ||||
| -rw-r--r-- | src/jsonval.c | 20 | ||||
| -rw-r--r-- | src/kdl.c | 7 |
4 files changed, 81 insertions, 22 deletions
@@ -151,8 +151,11 @@ typedef struct { char *err; size_t errcap; bool failed; + int depth; } parser_t; +#define MAX_DEPTH 256 + static void fail(parser_t *ps, const char *what) { if (ps->failed) return; ps->failed = true; @@ -193,9 +196,14 @@ static void parse_primary(parser_t *ps, expr_prog_t *pr) { if (ps->failed) return; if (*ps->p == '(') { + if (++ps->depth > MAX_DEPTH) { + fail(ps, "expression nested too deeply"); + return; + } ps->p++; parse_expr(ps, pr); skip_ws(ps); + ps->depth--; if (*ps->p != ')') { fail(ps, "expected )"); return; @@ -534,8 +542,10 @@ static int vm_run(const expr_prog_t *pr, const expr_env_t *env) { switch (in->op) { case OP_PUSH: st[sp++] = in->imm; break; case OP_VAR: st[sp++] = vars[in->imm]; break; + /* Negation wraps: -INT32_MIN is itself, as `x|0` maths in the JS + * preview would have it, rather than the undefined overflow it is in C. */ case OP_NEG: - if (sp) st[sp - 1] = -st[sp - 1]; + if (sp) st[sp - 1] = (int32_t)(0u - (uint32_t)st[sp - 1]); break; case OP_NOT: if (sp) st[sp - 1] = !st[sp - 1]; @@ -544,13 +554,14 @@ static int vm_run(const expr_prog_t *pr, const expr_env_t *env) { if (sp) st[sp - 1] = (int32_t)~(uint32_t)st[sp - 1]; break; case OP_ABS: - if (sp) st[sp - 1] = st[sp - 1] < 0 ? -st[sp - 1] : st[sp - 1]; + if (sp && st[sp - 1] < 0) + st[sp - 1] = (int32_t)(0u - (uint32_t)st[sp - 1]); break; case OP_SIN: if (sp) st[sp - 1] = isin(st[sp - 1]); break; case OP_COS: - if (sp) st[sp - 1] = isin(st[sp - 1] + 90); + if (sp) st[sp - 1] = isin((int32_t)((uint32_t)st[sp - 1] + 90u)); break; default: { int need = in->op == OP_CLAMP ? 3 @@ -560,26 +571,33 @@ static int vm_run(const expr_prog_t *pr, const expr_env_t *env) { if (sp < need) return 0; int32_t b = st[sp - 1], a = st[sp - 2]; switch (in->op) { + /* Arithmetic wraps as two's-complement int32, like `|0` in JS and + * unlike C, where signed overflow is undefined: `t` only grows, so + * a long-lived session's animation must not be able to walk an + * expression into UB. Done unsigned, where wrapping is defined. */ case OP_ADD: - st[sp - 2] = a + b; + st[sp - 2] = (int32_t)((uint32_t)a + (uint32_t)b); sp--; break; case OP_SUB: - st[sp - 2] = a - b; + st[sp - 2] = (int32_t)((uint32_t)a - (uint32_t)b); sp--; break; case OP_MUL: - st[sp - 2] = a * b; + st[sp - 2] = (int32_t)((uint32_t)a * (uint32_t)b); sp--; break; /* Defined rather than trapping: an expression that divides by zero - * should look wrong, not take the session with it. */ + * should look wrong, not take the session with it. INT32_MIN / -1 + * is the one other trapping division (SIGFPE on x86); it wraps to + * INT32_MIN like the other arithmetic, and the matching modulo + * is 0. */ case OP_DIV: - st[sp - 2] = b ? a / b : 0; + st[sp - 2] = !b ? 0 : (a == INT32_MIN && b == -1) ? a : a / b; sp--; break; case OP_MOD: - st[sp - 2] = b ? a % b : 0; + st[sp - 2] = !b ? 0 : (a == INT32_MIN && b == -1) ? 0 : a % b; sp--; break; case OP_LT: @@ -671,14 +689,24 @@ static int vm_run(const expr_prog_t *pr, const expr_env_t *env) { * ellipse, which is the first thing anyone notices. */ int32_t y2 = st[sp - 1], x2 = st[sp - 2], y1 = st[sp - 3], x1 = st[sp - 4]; - int32_t dx = x1 - x2, dy = (y1 - y2) * 2; - int32_t d2 = dx * dx + dy * dy; + /* 64-bit, because the operands are arbitrary int32 and squaring a + * difference of those overflows any int32 with room to spare -- + * and the components saturate at 2^16, because the result is + * capped at 4096 anyway and a doubled int32 difference squared + * would overflow even 64 bits. Unobservable except by not + * crashing. */ + int64_t dx = (int64_t)x1 - x2, dy = ((int64_t)y1 - y2) * 2; + if (dx > 65536) dx = 65536; + if (dx < -65536) dx = -65536; + if (dy > 65536) dy = 65536; + if (dy < -65536) dy = -65536; + int64_t d2 = dx * dx + dy * dy; /* Integer square root, so `dist` is a distance and not its * square: expressions that scale linearly with it are what people * write, and squaring is easy to ask for and hard to undo. */ - int32_t r = 0; + int64_t r = 0; while ((r + 1) * (r + 1) <= d2 && r < 4096) r++; - st[sp - 4] = r; + st[sp - 4] = (int32_t)r; sp -= 3; break; } diff --git a/src/input.c b/src/input.c index 47b4737..320b713 100644 --- a/src/input.c +++ b/src/input.c @@ -3,6 +3,7 @@ #include "input.h" #include <ghostty/vt.h> +#include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -28,6 +29,9 @@ void input_free(input_parser_t *p) { } static void buf_append(input_parser_t *p, const uint8_t *d, size_t n) { + /* input_timeout() re-parses the buffer via input_feed(p, NULL, 0), and + * memcpy with a NULL source is UB even for zero bytes. */ + if (n == 0) return; if (p->len + n > p->cap) { while (p->cap < p->len + n) p->cap *= 2; p->buf = realloc(p->buf, p->cap); @@ -273,8 +277,9 @@ static size_t parse_one(input_parser_t *p, input_cb_t cb, void *ud) { fi++; continue; } - if (params[k] >= '0' && params[k] <= '9') - f[fi] = f[fi] * 10 + (params[k] - '0'); + if (params[k] >= '0' && params[k] <= '9') { + if (f[fi] <= INT_MAX / 10) f[fi] = f[fi] * 10 + (params[k] - '0'); + } } input_event_t ev = {.kind = EV_MOUSE}; int btn = f[0]; @@ -310,10 +315,15 @@ static size_t parse_one(input_parser_t *p, input_cb_t cb, void *ud) { uint8_t ch = params[k]; if (ch >= '0' && ch <= '9') { any = true; - if (sub_i == 0) - nums[ncount] = nums[ncount] * 10 + (ch - '0'); - else if (sub_i <= 2) - subs[ncount][sub_i] = subs[ncount][sub_i] * 10 + (ch - '0'); + /* Saturate rather than overflowing: no real terminal parameter + * exceeds a few thousand, and INT_MAX is "big". */ + if (sub_i == 0) { + if (nums[ncount] <= INT_MAX / 10) + nums[ncount] = nums[ncount] * 10 + (ch - '0'); + } else if (sub_i <= 2) { + if (subs[ncount][sub_i] <= INT_MAX / 10) + subs[ncount][sub_i] = subs[ncount][sub_i] * 10 + (ch - '0'); + } } else if (ch == ':') { if (sub_i < 2) sub_i++; } else if (ch == ';') { diff --git a/src/jsonval.c b/src/jsonval.c index bd718b1..48cc870 100644 --- a/src/jsonval.c +++ b/src/jsonval.c @@ -1,6 +1,7 @@ #define _GNU_SOURCE #include "jsonval.h" +#include <limits.h> #include <stdlib.h> #include <string.h> @@ -80,9 +81,17 @@ static char *parse_string_raw(P *s, size_t *out_len) { case '"': *o++ = '"'; break; case '\\': *o++ = '\\'; break; case 'u': { + /* \uXXXX needs exactly four hex digits after the 'u'; a truncated + * escape must not read past the NUL terminator. */ + if (!s->p[1] || !s->p[2] || !s->p[3] || !s->p[4]) { + s->bad = true; + free(buf); + return NULL; + } unsigned cp = hex4(s->p + 1); s->p += 4; - if (cp >= 0xd800 && cp < 0xdc00 && s->p[1] == '\\' && s->p[2] == 'u') { + if (cp >= 0xd800 && cp < 0xdc00 && s->p[1] == '\\' && s->p[2] == 'u' && + s->p[3] && s->p[4] && s->p[5] && s->p[6]) { unsigned lo = hex4(s->p + 3); s->p += 6; cp = 0x10000 + ((cp - 0xd800) << 10) + (lo - 0xdc00); @@ -273,7 +282,14 @@ const char *jv_str(const jv_t *v, const char *fallback) { long jv_int(const jv_t *v, long fallback) { if (!v) return fallback; - if (v->kind == JV_NUM) return (long)v->num; + if (v->kind == JV_NUM) { + /* A double outside LONG_MIN..LONG_MAX is UB to cast. Clamp rather than + * returning fallback so a "big number" is still big and not silently + * replaced by a default. */ + if (v->num >= (double)LONG_MAX) return LONG_MAX; + if (v->num <= (double)LONG_MIN) return LONG_MIN; + return (long)v->num; + } if (v->kind == JV_STR) return strtol(v->str, NULL, 10); return fallback; } @@ -83,7 +83,12 @@ static char *parse_string(K *k) { while (*k->p && *k->p != '"') { char c = *k->p++; if (c == '\\') { - char e = *k->p++; + /* A backslash as the last byte must not step past the terminator: + * consuming the NUL would leave the loop reading out of bounds. Break + * and let the unterminated-string check below report it. */ + char e = *k->p; + if (!e) break; + k->p++; switch (e) { case 'n': c = '\n'; break; case 't': c = '\t'; break; |
