aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-19 22:48:31 +0200
committeruser <user@clank>2026-08-19 22:48:31 +0200
commit009350acbcd4360680b8fa228d8cd03a2f2b5e93 (patch)
treeef0eadb14cc8186829c4b7ce46955ea88443a405
parentwebdemo: the theme symlink is a dependency of the image (diff)
downloadslosh-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.c54
-rw-r--r--src/input.c22
-rw-r--r--src/jsonval.c20
-rw-r--r--src/kdl.c7
4 files changed, 81 insertions, 22 deletions
diff --git a/src/expr.c b/src/expr.c
index fd88f9c..272f8d7 100644
--- a/src/expr.c
+++ b/src/expr.c
@@ -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;
}
diff --git a/src/kdl.c b/src/kdl.c
index 13d624f..41f9768 100644
--- a/src/kdl.c
+++ b/src/kdl.c
@@ -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;