Skip to content

Guard MODULO against the undefined INT_MIN % -1 (SIGFPE that kills the IOC)#38

Open
physwkim wants to merge 1 commit into
epics-modules:masterfrom
physwkim:fix/modulo-intmin-sigfpe
Open

Guard MODULO against the undefined INT_MIN % -1 (SIGFPE that kills the IOC)#38
physwkim wants to merge 1 commit into
epics-modules:masterfrom
physwkim:fix/modulo-intmin-sigfpe

Conversation

@physwkim

Copy link
Copy Markdown

The calc engines evaluate % as a signed integer remainder on values narrowed from double. Each MODULO case guards a zero divisor but none guards INT_MIN % -1 (and INT64_MIN % -1 on LP64) — undefined in C, and on x86 the idiv behind % traps the quotient overflow as #DE/SIGFPE. Because an out-of-range or NaN double casts to 0x80000000 = INT_MIN, an ordinary data-driven expression (a large counter or ADC sum, a NaN) against a -1 divisor crashes the whole IOC.

Five sites are affected: sCalcPerform.c numeric ((int)) and string-stack ((long)) paths, and aCalcPerform.c scalar / array%scalar / array%array paths. Fixed uniformly by routing each remainder through a safeModulo() helper that returns 0 for a -1 divisor (x % -1 == 0 for all x), leaving the zero-divisor guards and all ordinary remainders unchanged.

Verified with a driver linking the real sCalcPerform/aCalcPerform translation units and driving every site through the public postfix+perform API: all five SIGFPE before the change and return 0 after; 17 % 5 returns 2 unchanged.

The same defect exists in epics-base's calcPerform.c MODULO; a separate fix is being submitted there.

The calc engines compute '%' as a signed integer remainder on values
narrowed from double.  Every MODULO case guards a zero divisor but none
guards the one remaining case C leaves undefined: INT_MIN % -1 (and
INT64_MIN % -1 on LP64).  On x86 the idiv instruction that implements
'%' traps the quotient overflow as #DE, delivered as SIGFPE, so a single
calc/scalc/acalc expression whose dividend narrows to INT_MIN (readily
produced by an out-of-range or NaN double, which casts to 0x80000000)
against a -1 divisor takes down the whole IOC.

Mathematically x % -1 == 0 for every x, so route each remainder through
a small safeModulo() helper that returns 0 for a -1 divisor instead of
executing the trapping idiv.  This closes the family at every site with
one rule rather than a per-site patch:

  sCalcPerform.c  numeric path  (int)  and  string path  (long)
  aCalcPerform.c  scalar, array%scalar, and array%array paths (int)

The zero-divisor guards are unchanged; only the -1 case, previously
undefined behaviour, is now defined to 0.  Ordinary remainders are
unaffected.

Proved with a driver linking the real sCalcPerform/aCalcPerform
translation units and driving each site through sCalcPostfix/aCalcPostfix
+ Perform, one fork per case (INT_MIN dividend, -1 divisor):

  site                      before            after
  sCalc numeric  (:562)     SIGFPE            0
  sCalc string   (:1109)    SIGFPE            0   (post0==USES_STRING)
  aCalc scalar   (:701)     SIGFPE            0
  aCalc arr%sca  (:674)     SIGFPE            0
  aCalc arr%arr  (:650)     SIGFPE            0
  control 17 % 5            2                 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant