calc engine: fix four out-of-bounds accesses in the sCalc/aCalc perform paths - #41
Open
physwkim wants to merge 4 commits into
Open
calc engine: fix four out-of-bounds accesses in the sCalc/aCalc perform paths#41physwkim wants to merge 4 commits into
physwkim wants to merge 4 commits into
Conversation
RIGHT_SHIFT/LEFT_SHIFT clamped the character-shift count above at SCALC_STRING_SIZE but never below. A negative count makes the string branch read/write past the 40-byte local_string: RIGHT_SHIFT reads ps->s[i-j] (j<0) beyond the buffer, and LEFT_SHIFT runs i up to SCALC_STRING_SIZE-j (>40), writing ps->s[i] into the adjacent stack cell. Clamp the count to [0, SCALC_STRING_SIZE] with the same myMAX(myMIN(...)) idiom already used by the SUBRANGE bounds below.
The LITERAL_STRING copy loop was written as a bounded copy — for (i=0; (i<SCALC_STRING_SIZE-1) && *post; ) — but i was never incremented, so the i<39 bound stayed permanently true and the copy ran to the end of the literal. A quoted literal longer than 39 characters therefore overran the 40-byte local_string inside the stack element. Increment i in the loop so the intended bound applies.
j is an inclusive array index but was clamped to arraySize, which the element count itself (e.g. AA[3,N]) reaches. The SUBRANGE copy loop for (k=0; i<=j; k++, i++) ps->a[k] = ps->a[i] then reads ps->a[arraySize], one past the arraySize-element buffer. Cap j at arraySize-1 so the inclusive loop stops at the last valid element; this also corrects the SUBRANGE_IP numEl (j+1) which over-reported by one at the boundary.
nderiv() fixes the fit window at m = 2*npts+1 (5 for deriv()) and called fitpoly(x,y,m,...) plus the tail loop lx[j]=x[(n-m)+j] regardless of the caller's point count n. With n<m the fitpoly accumulation reads x[0..m-1]/y[0..m-1] past the operand, and the tail loop indexes x[(n-m)+j] with a negative offset. Guard n<m up front and return the same -1 error the fit helpers already use, matching the if (n<3) return(-1) idiom in fitpoly()/pfit().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
calc engine: fix four out-of-bounds accesses in the sCalc/aCalc perform paths
Summary
Four memory-safety defects in the calc expression engines, each reachable
from an ordinary
.db-authored expression (no debug build, no specialbuild flags). Three are out-of-bounds reads/writes past a fixed-size
buffer; one is an inert copy bound that lets a long string literal overrun
its stack cell. Each fix is a one-line, minimal guard that matches a guard
idiom already present in the same file. No refactoring, no behavior change
on the in-range paths.
One commit per defect:
sCalc: clamp the shift count against a negative operandcalcApp/src/sCalcPerform.c<</>>with a negative countsCalc: bound the LITERAL_STRING copy by incrementing its countercalcApp/src/sCalcPerform.caCalc: cap the SUBRANGE upper bound at arraySize-1calcApp/src/aCalcPerform.cSUBRANGEcalcUtil: reject an nderiv fit window larger than the arraycalcApp/src/calcUtil.cDERIVon arrays shorter than 5Details
1.
sCalcPerform.c—<</>>with a negative shift count (OOB read/write)In the
RIGHT_SHIFT/LEFT_SHIFTcase the character-shift count iscomputed and clamped only from above:
For a negative
j(the shift operand evaluates negative at runtime)the string branch runs off the 40-byte
local_string:RIGHT_SHIFT:ps->s[i] = (i>=j)?ps->s[i-j]:' '— withj<0thecondition
i>=jis always true, so it readsps->s[i-j], i.e.ps->s[i+|j|], past the end of the buffer.LEFT_SHIFT: the loop bound becomesi < SCALC_STRING_SIZE - j(
= 40 + |j|), sops->s[i]is written out tos[40+|j|-1]— anout-of-bounds write into the adjacent stack element.
Reachability: any scalcout expression of the form
AA << B(or>>)whose
Boperand goes negative at runtime — an ordinary evaluation, not amalformed database.
Fix: clamp the count to
[0, SCALC_STRING_SIZE]using the samemyMAX(myMIN(...),0)idiom this file already uses for theSUBRANGEbounds (
aCalcPerform.c:1533). This closes the OOB read/write on thestring branch. The pure-numeric
(int)>>(int)branch shifts by theuntouched operand and is left as-is: a negative numeric shift count is
C-language UB but not a memory-safety issue, and clamping it would change
long-standing numeric results.
2.
sCalcPerform.c—LITERAL_STRINGcopy bound never advances (OOB write)The loop was written as a bounded copy, but
iis never incremented, soi < SCALC_STRING_SIZE-1(i.e.i < 39) stays permanently true and thecopy runs to the end of the literal. A quoted literal longer than 39
characters therefore writes past
local_string[40]inside the stackelement, on every evaluation.
Reachability: anyone who can load a database — a single long quoted
string literal in an sCalcout/scalcout expression.
Fix: increment
iin the loop so the intendedSCALC_STRING_SIZE-1bound actually applies (the copy stops at 39 chars,
*s='\0'terminates).3.
aCalcPerform.c—SUBRANGEupper bound admitsarraySize(OOB read)ps->aholdsarraySizedoubles.jis an inclusive upper index butis clamped to
arraySize, a value the element count itself reaches (e.g.AA[3,N]whereNis the array length). The inclusive copy loop thenreads
ps->a[arraySize], one element past the buffer. Under ASAN, or anunlucky allocation, this crashes the IOC on a legal expression.
Fix: cap
jatarraySize-1, the last valid index. This alsocorrects the
SUBRANGE_IPelement countnumEl = j+1, whichover-reported by one at the same boundary. The analogous string
SUBRANGEinsCalcPerform.cis not affected — its copy loop isguarded by the string's NUL terminator (
*s1), so reaching indexstrlenreads the in-bounds terminator; only the array path lacks aterminator, so only the array clamp is changed.
4.
calcUtil.c—DERIV/nderivfixed 5-point window over-reads short arrays (OOB read)nderiv()fixes its fit window atm = 2*npts+1(deriv()passesnpts=2, som=5) and then unconditionally does:fitpoly(x, y, m, ...), whose accumulation readsx[0..m-1]/y[0..m-1]regardless of the caller's actual point count
n;lx[j] = x[(n-m)+j] - x[n-m]which, forn<m, indexesx[]with a negative offset.nderivnever comparesmwithn, andfitpoly's only guard (n<3)tests its own argument, which is the constant
m. aCalc reaches this with1+lastEl-firstElpoints, which a 2-element array makes2— soDERIV(AA)on an array (or window) shorter than 5 reads before/past theoperand buffer.
Fix: guard
n < mat the top ofnderivand return the same-1error the fit helpers already return (
if (n<3) return(-1)infitpoly()/pfit(),if (n<2) return(-1)inlfit()). Both callers(
deriv()ataCalcPerform.c:985and thenderiv-with-nptscase at:613) route through this one function, so the single guard covers both;the
:613caller already constrainsnptsso thatm <= n, so the guardis a no-op on that path and only rejects the genuinely-too-short
deriv()case.
Testing
Not compiled locally — this checkout has no EPICS base support tree
configured, so
makewas not run here. Each change is a single-line guardverified by reading the surrounding code and the macro expansions:
myMAX(myMIN(j,SCALC_STRING_SIZE),0)form is byte-for-byte the samemacro nesting already used at
aCalcPerform.c:1533, so it carries no newprecedence risk.
j = myMIN(j,arraySize-1)andif (n < m) return(-1);are trivial andmirror existing idioms in the same files.
LITERAL_STRINGfix only adds the missingi++to theforincrement clause.
CI will compile all targets. These are guard additions on
out-of-range/negative inputs and do not change results on any in-range
path, so existing regression expectations are preserved.