From f207871139eea528e3300bc7e72dd648a6cce983 Mon Sep 17 00:00:00 2001 From: Sang Woo Kim Date: Fri, 17 Jul 2026 23:53:37 +0900 Subject: [PATCH] aCalc: fix off-by-two in INC() runtime stack-overflow guard The value stack is calloc(ACALC_STACKSIZE) with top = &stack[1], so the last writable element is stack[ACALC_STACKSIZE-1]. INC() advanced ps, then guarded with `(ps-top) > ACALC_STACKSIZE` before writing (ps)->numEl and (ps)->sourceDouble. Since ps addresses stack[1 + (ps-top)], the write lands out of bounds once (ps-top) reaches ACALC_STACKSIZE-1: the guard let indices ACALC_STACKSIZE and ACALC_STACKSIZE+1 be written before tripping. The postfix compiler caps a purely scalar expression at ACALC_STACKSIZE, so scalar depth alone never reaches the guard. NDERIV/DERIV/FITPOLY, however, push two or three scratch elements at run time (aCalcPerform.c NDERIV does DEC then three INCs) that the compiler's per-opcode `runtime_effect` accounting models only as a net delta, not the transient peak. A deep-enough expression such as `1+(1+(...+(NDERIV(AA,2))...))` therefore passes aCalcPostfix() yet drives the runtime stack past its end, writing out of bounds and crashing the IOC. Guard on `(ps-top) > ACALC_STACKSIZE-2` so INC() rejects before the write can exceed stack[ACALC_STACKSIZE-1]. Valid depths are unchanged. Add acalcTest cases: 15 levels of nesting around NDERIV evaluate; 16 levels are rejected by the runtime guard. Without the fix the k=16 case writes out of bounds and terminates acalcTest with SIGSEGV. --- calcApp/src/aCalcPerform.c | 2 +- tests/acalcTest.cpp | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/calcApp/src/aCalcPerform.c b/calcApp/src/aCalcPerform.c index 66f5e60..87c34d0 100644 --- a/calcApp/src/aCalcPerform.c +++ b/calcApp/src/aCalcPerform.c @@ -86,7 +86,7 @@ int aCalcStackLW = 0; /* low-water mark */ ++ps; \ if ((int)((ps)-top) > aCalcStackHW) \ aCalcStackHW = (int)((ps)-top); \ - if ((ps-top)>ACALC_STACKSIZE) { \ + if ((ps-top)>ACALC_STACKSIZE-2) { \ printf("aCalcPerform:stack overflow\n"); \ freeStack(flp, stack); return(-1); \ } else { \ diff --git a/tests/acalcTest.cpp b/tests/acalcTest.cpp index bdbecb6..45579dc 100644 --- a/tests/acalcTest.cpp +++ b/tests/acalcTest.cpp @@ -92,6 +92,32 @@ static void testAValExpr(const char* expr, double* args, double** aargs, double* //free(rpn); } +/* Assert whether an expression the postfix compiler accepts overflows the + * runtime value stack. Operators such as NDERIV/DERIV/FITPOLY push scratch + * elements at run time that the compiler's per-opcode stack accounting does + * not model, so a deep-enough expression passes aCalcPostfix() yet drives the + * runtime stack past its end. The INC() guard must reject it (aCalcPerform + * returns non-zero) instead of writing out of bounds. */ +static void testStackOverflow(const char* expr, double* args, double** aargs, bool expectOverflow) +{ + unsigned char rpn[255]; + short err; + + double val = 0.0; + double aval[12] = {0.0}; + epicsUInt32 amask; + + if (aCalcPostfix(expr, rpn, &err)) + { + testOk(false, "postfix rejected '%s': %s", expr, aCalcErrorStr(err)); + return; + } + + bool overflow = (aCalcPerform(args, 12, aargs, 12, 12, &val, aval, rpn, 1, &amask) != 0); + testOk(overflow == expectOverflow, "%s -> %s", expr, + overflow ? "runtime stack overflow rejected" : "evaluated"); +} + MAIN(acalcTest) { @@ -125,7 +151,7 @@ MAIN(acalcTest) double* aargs[12] = {AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL}; - testPlan(148); + testPlan(150); testValExpr("finite(1)", args, aargs, 1); testValExpr("finite(AA)", args, aargs, 1); @@ -353,6 +379,16 @@ MAIN(acalcTest) /* Reset B */ args[1] = B; + /* Runtime value-stack overflow guard (INC off-by-two). NDERIV pushes + * three scratch elements the compiler does not account for; 15 levels of + * nesting evaluate, 16 must be rejected by the runtime guard rather than + * writing past stack[ACALC_STACKSIZE-1]. Without the fix the k=16 case + * writes out of bounds and crashes the test binary. */ + testStackOverflow("1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(NDERIV(AA,2))))))))))))))))", + args, aargs, false); + testStackOverflow("1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(NDERIV(AA,2)))))))))))))))))", + args, aargs, true); + /* Parse error tests */ { unsigned char rpn[255];