commit f2c14c6b380edb531729aa4cd195b4a6ec02c27f
parent 1dae315948770c1020a5231df4d4a7698b48aa7c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 14:36:04 -0700
link: bound link-script expression recursion in parse and eval
Deeply nested expressions (e.g. (((...))) ) recursed unbounded through
parse_atom/parse_expr/parse_binop_rhs and eval_link_expr, risking a C
stack overflow on adversarial scripts. Add a depth counter to the LSP
parser state (guarded in a parse_atom wrapper around parse_atom_inner)
and a depth parameter to eval (eval_link_expr_rec, with a thin
eval_link_expr wrapper entering at depth 0). Both fail cleanly past 256:
the parser emits a diagnostic and unwinds; eval sets *err.
Adds check_recursion_depth_guard (512 nested parens -> clean rejection,
no crash; red->green).
Diffstat:
2 files changed, 74 insertions(+), 33 deletions(-)
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -688,13 +688,22 @@ static void validate_script_target(Linker* l, const KitLinkScript* script) {
}
}
-static u64 eval_link_expr(Linker* l, LinkImage* img, u64 dot,
- const ScriptOutInfo* outs, u32 nouts,
- const KitLinkExpr* e, int* err) {
+/* Maximum link-script expression evaluation depth — mirrors the parser's
+ * LSP_MAX_EXPR_DEPTH so eval fails cleanly (sets *err) instead of overflowing
+ * the stack on a pathologically nested expression. */
+#define EVAL_LINK_MAX_DEPTH 256
+
+static u64 eval_link_expr_rec(Linker* l, LinkImage* img, u64 dot,
+ const ScriptOutInfo* outs, u32 nouts,
+ int depth, const KitLinkExpr* e, int* err) {
if (!e) {
*err = 1;
return 0;
}
+ if (depth > EVAL_LINK_MAX_DEPTH) {
+ *err = 1;
+ return 0;
+ }
switch ((KitLinkExprKind)e->kind) {
case KIT_LE_INT:
return (u64)e->v.int_val;
@@ -711,61 +720,61 @@ static u64 eval_link_expr(Linker* l, LinkImage* img, u64 dot,
return LinkSyms_at(&img->syms, id - 1)->vaddr;
}
case KIT_LE_NEG:
- return (u64)(-(i64)eval_link_expr(l, img, dot, outs, nouts,
- e->v.align.val, err));
+ return (u64)(-(i64)eval_link_expr_rec(l, img, dot, outs, nouts,
+ depth + 1, e->v.align.val, err));
case KIT_LE_ADD:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) +
- eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) +
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_SUB:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) -
- eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) -
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_MUL:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) *
- eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) *
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_DIV: {
- u64 rhs = eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ u64 rhs = eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
if (rhs == 0) {
*err = 1;
return 0;
}
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) / rhs;
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) / rhs;
}
case KIT_LE_AND:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) &
- eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) &
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_OR:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) |
- eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) |
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_XOR:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) ^
- eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) ^
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_SHL:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err)
- << eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err)
+ << eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_SHR:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err) >>
- eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err) >>
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
case KIT_LE_MAX: {
- u64 a = eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err);
- u64 b = eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ u64 a = eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err);
+ u64 b = eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
return a > b ? a : b;
}
case KIT_LE_MIN: {
- u64 a = eval_link_expr(l, img, dot, outs, nouts, e->v.bin.lhs, err);
- u64 b = eval_link_expr(l, img, dot, outs, nouts, e->v.bin.rhs, err);
+ u64 a = eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.lhs, err);
+ u64 b = eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.bin.rhs, err);
return a < b ? a : b;
}
case KIT_LE_ALIGN: {
- u64 v = eval_link_expr(l, img, dot, outs, nouts, e->v.align.val, err);
+ u64 v = eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.align.val, err);
u64 a =
- eval_link_expr(l, img, dot, outs, nouts, e->v.align.align, err);
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.align.align, err);
if (a == 0) return v;
return ALIGN_UP(v, a);
}
case KIT_LE_BLOCK: {
- u64 v = eval_link_expr(l, img, dot, outs, nouts, e->v.align.val, err);
+ u64 v = eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.align.val, err);
u64 a =
- eval_link_expr(l, img, dot, outs, nouts, e->v.align.align, err);
+ eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.align.align, err);
if (a == 0) return v;
return ALIGN_UP(v, a);
}
@@ -822,7 +831,7 @@ static u64 eval_link_expr(Linker* l, LinkImage* img, u64 dot,
return (s && s->defined) ? 1u : 0u;
}
case KIT_LE_ABSOLUTE:
- return eval_link_expr(l, img, dot, outs, nouts, e->v.align.val, err);
+ return eval_link_expr_rec(l, img, dot, outs, nouts, depth + 1, e->v.align.val, err);
default:
compiler_panic(l->c, SRCLOC_NONE,
"linker script: expression kind %u not supported",
@@ -831,6 +840,13 @@ static u64 eval_link_expr(Linker* l, LinkImage* img, u64 dot,
}
}
+/* Depth-tracked entry point: callers evaluate from depth 0. */
+static u64 eval_link_expr(Linker* l, LinkImage* img, u64 dot,
+ const ScriptOutInfo* outs, u32 nouts,
+ const KitLinkExpr* e, int* err) {
+ return eval_link_expr_rec(l, img, dot, outs, nouts, 0, e, err);
+}
+
/* Set a segment's phdr attributes (type/filehdr/phdrs/flags) from the named
* PHDRS entry `name`. Panics if the name is unknown. `dot`/`outs`/`nouts` are
* forwarded to the FLAGS() expression evaluation. */
diff --git a/src/link/link_script.c b/src/link/link_script.c
@@ -73,8 +73,15 @@ typedef struct LSP {
/* one-bit error sticky: any diagnostic flips this and the parser
* unwinds without producing partial output. */
int err;
+ /* expression-parse recursion depth (parens / unary / binop RHS); guards the
+ * descent from overflowing the C stack on pathological input. */
+ int expr_depth;
} LSP;
+/* Maximum expression nesting depth for both parse and eval. Past this we emit
+ * a clean diagnostic instead of recursing into a stack overflow. */
+#define LSP_MAX_EXPR_DEPTH 256
+
/* ---- diagnostics ---- */
static SrcLoc lsp_loc(const LSP* p, size_t off) {
@@ -336,6 +343,7 @@ static int match_kw(LSP* p, const char* kw) {
*/
static KitLinkExpr* parse_expr(LSP* p);
+static KitLinkExpr* parse_atom(LSP* p);
static KitLinkExpr* parse_int(LSP* p) {
KitLinkExpr* e;
@@ -396,7 +404,7 @@ static KitLinkExpr* parse_int(LSP* p) {
return e;
}
-static KitLinkExpr* parse_atom(LSP* p) {
+static KitLinkExpr* parse_atom_inner(LSP* p) {
int ch;
skip_ws(p);
if (p->err) return NULL;
@@ -570,6 +578,23 @@ static KitLinkExpr* parse_atom(LSP* p) {
return NULL;
}
+/* Depth-guarded entry to atom parsing. Every nesting level (parens, unary,
+ * binop RHS, helper-call argument) descends through parse_atom, so bumping the
+ * counter here bounds total expression-parse recursion. */
+static KitLinkExpr* parse_atom(LSP* p) {
+ KitLinkExpr* e;
+ if (p->err) return NULL;
+ if (++p->expr_depth > LSP_MAX_EXPR_DEPTH) {
+ lsp_errf(p, p->pos, "linker-script expression nested too deeply (>%d)",
+ LSP_MAX_EXPR_DEPTH);
+ --p->expr_depth;
+ return NULL;
+ }
+ e = parse_atom_inner(p);
+ --p->expr_depth;
+ return e;
+}
+
/* Returns >=0 binding power for a binary operator at p->pos and
* advances past it; -1 if no binary operator at the lookahead. */
static int try_take_binop(LSP* p, KitLinkExprKind* out_kind) {