Skip to content

Issue 80 fix - #83

Open
Neoky wants to merge 1 commit into
cesanta:masterfrom
Neoky:fix/toint32-operator-coercion
Open

Neoky wants to merge 1 commit into
cesanta:masterfrom
Neoky:fix/toint32-operator-coercion

Conversation

@Neoky

@Neoky Neoky commented Oct 15, 2026

Copy link
Copy Markdown

Hi, I'm Neoky, and I'm a cyber researcher. I've been developing a new tool to check for type safety and it found some issues with elk that I've patched.

do_op: apply the ToInt32/ToUint32 coercion the language specifies

Fixes #80.

do_op() converts operands with (long) before computing. That is undefined in C
whenever the value is out of range (C11 6.3.1.4) or a shift count is negative or
≥ the operand width (C11 6.5.7), and for ===/!== it is also simply wrong —
(long)1.5 == (long)1.7, so 1.5 === 1.7 is currently true.

ECMAScript specifies these operators: &, |, ^, ~, <<, >> apply
ToInt32/ToUint32 (a defined modulo-2^32 conversion), the shift operators mask the
count to its low five bits, and === compares doubles. This is that coercion, not
hardening added on top.

Changes

  • & | ^ ~ << >> go through to_int32/to_uint32; shift counts are
    masked to 5 bits, and << goes through the unsigned type widened to 64 bits so
    integer promotion cannot overflow an int.
  • === / !== compare as doubles, like the relational operators already do.
  • % keeps computing a - b * trunc(a / b); only the undefined (long) cast is
    replaced. Operands that would yield a NaN are refused, the way TOK_DIV already
    refuses a zero divisor — see the note on Type Confusion: Positive NaN is misinterpreted as T_CODEREF #72 below.

Behavior changes

These are user-visible. Each is the answer the specification gives:

expression before after
2147483648 | 0 2147483648 -2147483648
4294967296 | 0 4294967296 0
1e21 | 0 -9.2233720368547758e+18 -559939584
~2147483648 -2147483649 2147483647
1 << 31 2147483648 -2147483648
1 << 32 4294967296 1
1.5 === 1.7 true false
0.5 === 0 true false
5 % 0 5 ERROR: bad % operands

Undefined behavior is also removed where the observed output does not change —
1 >> (0 - 1) and 1 << 66 both print the same value before and after, but only
the second is defined.

No new dependency

elk links no math library on any target, so ToUint32 reads the exponent and
mantissa out of the double rather than calling fmod()/trunc(). nm shows no
fmod, trunc, pow or sqrt in the object. The bit version was checked against
fmod(trunc(d), 2^32) over two million random doubles plus the boundary cases, with
no difference.

That is also why % is not an exact remainder: fmod() would be, and fmod() means
libm. If you would rather have exact % and accept -lm, it is a one-line change —
happy to switch it.

Testing

  • make -C test test and make -C test test++ pass. C++ warning count is unchanged
    at 1 (the pre-existing -Wdangling-pointer on js->cstk = &res in js_eval).
  • Not run locally: uno, vc2017, vc22, mingw.
  • Unrelated heads-up: on a current toolchain make -C test test fails at
    unit_test.c:193 on master as well. ASan relocates locals to its fake stack, so
    the css probe in setlwm() ((char *) js->cstk - (char *) &n) returns 0 and the
    c > 0 assertion fires. ASAN_OPTIONS=detect_stack_use_after_return=0 restores it
    (css 0 → 2112). Not caused by this PR; glad to send it as a separate one-liner.

Not addressed

The value representation, which is #71 and #72: is_nan() requires the sign bit to
be clear, so a positive NaN or inf decodes as T_CODEREF rather than as a number, and
1e308 * 10 evaluates to an empty object. % refusing NaN-producing operands works
around that rather than fixing it; a real fix needs a representation that can hold NaN.

…casting to long

The remainder, bitwise and shift operators reached their integer operands by casting a
double straight to long, which is undefined whenever the value does not fit, and they
shifted by an unmasked count. UBSan reports float-cast-overflow and invalid-shift across
nine sites in do_op. ECMAScript specifies ToInt32/ToUint32 for these operators and a
shift count masked to five bits, so this is the conversion the language already requires.

Three sites needed more than a substitution: % is a floating point remainder in JS and
the cast was of the quotient, << has to go through the unsigned type, and == compared as
integers, which made 1.5 === 1.7 true. ToUint32 reads the exponent and mantissa directly
rather than calling fmod() and trunc(), because elk links no math library on its targets.
@Neoky
Neoky force-pushed the fix/toint32-operator-coercion branch from 1991a64 to bf6c685 Compare October 16, 2026 19:44
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.

=== truncates numbers to long (1.5 === 1.2 is true); % 0 and oversized shifts are wrong / UB

1 participant