Closed Bug 1763012 Opened 2 years ago Closed 2 years ago

Differential testing: miscompute related to div bails

Categories

(Core :: JavaScript Engine: JIT, defect, P2)

defect

Tracking

()

RESOLVED FIXED
102 Branch
Tracking Status
firefox-esr91 --- wontfix
firefox100 --- wontfix
firefox101 --- wontfix
firefox102 --- fixed

People

(Reporter: lukas.bernhard, Assigned: iain)

References

(Blocks 1 open bug)

Details

(Keywords: sec-audit, Whiteboard: [post-critsmash-triage][adv-main102-])

Attachments

(1 file)

During differential fuzzing, I encountered a miscomputation. The attached sample computes different values, depending on whether ion is enabled or not. Reproduces on git commit: 73a6abf1aaedbf7613fa90a7f459a8c0dfe5f0ce

Precautionary flagging as s-s because the code optimization fails to insert a necessary guard.

sample.js:

function main() {

  let v35;
  let counter = 0;
  let counter2 = 0;

  function f() {
      let v28 = 0;
      do {
          let v29 = v28++;
          v29 /= v29; 
          const v30 = --v29;
          let v31 = 0; 
          while (v31 < 6) {
              const v33 = ++v31;
              switch (v30) {
              case v30: // generates an MCompare (LICM'ed)
                  const v34 = v33 >= 8;
                  v35 = v34 ? 10 : 100;
                  counter += 1;
                  break;
              default:
                  // DCE'd in ion
                  counter2 += 1;
                  break;
              }
          }
      } while (v28 < 8);
  }
  
  f();
  f();
  
  print(counter);  // 90 with ion, 84 without
  print(counter2); // 6  with ion, 12 without
}

main();
obj-x86_64-pc-linux-gnu/dist/bin/js  --fast-warmup --no-threads --cpu-count=1 --ion-offthread-compile=off --fuzzing-safe --ion-range-analysis=on --ion-gvn=on sample.js

prints 90, 6

obj-x86_64-pc-linux-gnu/dist/bin/js  --fast-warmup --no-threads --cpu-count=1 --ion-offthread-compile=off --fuzzing-safe --differential-testing --no-ion sample.js

prints 84,12

Bisecting the issue identifies commit 26b5e252bc45b53394637f438eddd1300229912c related to bug 1637946.
On this old version of spidermonkey the required commandline for reproducing is:

obj-x86_64-pc-linux-gnu/dist/bin/js  --no-threads --cpu-count=1 --ion-offthread-compile=off --fuzzing-safe --ion-range-analysis=on --ion-gvn=on --warp --blinterp-warmup-threshold=4 --baseline-warmup-threshold=10 --ion-warmup-threshold=30 sample.js

My understanding of whats happening:
When OSRing, v29 is an MIRType::Int32. The switch generates an MCompare, comparing v30 with itself. This compare gets hoisted out of the inner loop and evaluates to true (because comparing an int32 with itself is always true). Hence the default block gets DCE'd and instead we always execute the "case v30".
v30 now has no more live users and doesn't need to be computed at all. Therefore v29 has no more live users. The division (v29/=29) should still bail because the correctness of the switch code depends on this. However, neither Guard nor GuardRangeBailout are set and somehow (???) the div vanishes during codegen.

Group: core-security → javascript-core-security

Jan, can you look at this issue, as this seems to be related to bug 1637946.

Severity: -- → S3
Flags: needinfo?(jdemooij)
Priority: -- → P2

Jan told me Iain was going to look into this bug so I am changing the NI to Iain.

Flags: needinfo?(jdemooij) → needinfo?(iireland)

This is kind of a mess.

Bug 1637946 isn't at fault here; it just let us optimize enough to expose an existing issue.

Simplifying, we have:

1 phi ... (Int32)
2 div phi1:Int32 phi1:Int32 (Int32)
3 toDouble div2:Int32 (Double)
4 compare toDouble3:Double toDouble3:Double (Bool)
5 test compare4:Bool block1 block2

Range analysis concludes that the range of the div, if it does not bail out, is [INT_MIN, INT_MAX] (aka it does not include NaN). Therefore, the range of the toDouble also does not include NaN, which lets us set operandsAreNeverNan_ on the MCompare. In turn, that lets us fold away the MCompare.

But once we've folded that away, the div has no uses, so we decide that it's dead code.

We don't really have a great way to express that we need the bailouts for an instruction, but not its actual result. Marking it as isGuard or isGuardRangeBailouts is probably the best existing option. I'm not sure where the best place to set the flag would be; unfortunately the isDouble serves as a level of indirection between the producer (div) and consumer (compare).

nbp, any thoughts?

(At first look I think this probably isn't exploitable: we get the wrong result, but the entire compilation agrees on what that result is, so I don't see a way to turn this into eg an out-of-bounds read. Not 100% confident, though.)

Flags: needinfo?(iireland) → needinfo?(nicolas.b.pierron)

Hypothetical reasoning, If the division instruction was not a single instruction, but split into bailing instructions which are checking whether we are in a corner case scenario and the executing code for the division once we eliminated all the others. Then it would be safe to elide the division computation, but not the bailing branch.

My understanding would be that as soon as MDiv range is computed, we should use the MDiv::fallible() function to set the GuardRangeBailout flag. This way, we can remove unused infallible MDiv instructions, while keeping these instructions around. Unfortunately, this is an approximation, as most of the time, NaN might not matter in the computation, but we would still be forced to keep it around.

The ultimate solution would be along the lines of keeping 2 Ranges, one with bailouts as we do today, and one without bailouts. If a removal decision taken from the one with bailout would be different than the one taken without bailout, then we know that one of the input is fallible and that it should be marked as a GuardRangeBailout.

In terms of being exploitable, what might happen is that a branch, which is not supposed to be taken, is taken with a right-hand-side value of the removed MDiv instruction being 0. Range Analysis only does forward propagation of range information, and does not create any guards filtering out values of inputs. Thus the Range of MDiv inputs remain untouched, and propagated as before in all branches, even the one which is not supposed to be taken. Thus, while incorrect, this should not yield any out-of-bound which are not already guarded against.

Flags: needinfo?(nicolas.b.pierron)

Nicolas: are you confident enough to unhide this as "not a security bug", or should we keep it hidden and call it sec-audit? If something worse comes to mind we could then switch that to an appropriate vulnerability rating.

Flags: needinfo?(nicolas.b.pierron)

(In reply to Daniel Veditz [:dveditz] from comment #5)

Nicolas: are you confident enough to unhide this as "not a security bug", or should we keep it hidden and call it sec-audit? If something worse comes to mind we could then switch that to an appropriate vulnerability rating.

I am confident that this issue is not a problem, but I suggest we wait before opening this bug, as the solution described in comment 4 applies to more instructions than MDiv.

While the conclusion is most likely identical, it is probably wiser to keep this issue as sec-audit.

Flags: needinfo?(nicolas.b.pierron)
Keywords: sec-audit

Differential fuzzing found another instance of the issue, this time using MMod instead of MDiv

function main() {
    for (v7 of "aaaaaa") {
        for (let v13 = 0; v13 < 100; v13++) { } 
    }   

    const v17 = [0,0,0];
    let v18 = 0;
    function v20() {
        for (let v48 = 0; v48 < 100; v48++) {
            const v49 = 0 % v48;
            [..."aaaaaaaaa",...v17];
            const v62 = v49 === v49;
            if (v62) {
                v18 += 1;
            }   
        }   
    }
    v20();
    v20();
    print(v18); // 198 w/o ion, 199 with ion
}
main();

This is a fairly conservative fix, but I didn't see any slowdown on Octane or Speedometer.

Assignee: nobody → iireland
Status: NEW → ASSIGNED

(In reply to Sebastian Hengst [:aryx] (needinfo me if it's about an intermittent or backout) from comment #9)

Backed out for causing assertions and test failures in Recover.cpp / dce-with-rinstructions.js:

I guess the options, are to either refined the Range info by using beta nodes on i argument, to explicitly filter out unexpected ranges. Such as:

i = i | 0;
if (i < 1) return i;
var x = 1 / i;

Good call, that fixes the testcase.

Flags: needinfo?(iireland)
Group: javascript-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 2 years ago
Resolution: --- → FIXED
Target Milestone: --- → 102 Branch

Since the status are different for nightly and release, what's the status for beta?
For more information, please visit auto_nag documentation.

This can ride the trains.

Flags: qe-verify-
Whiteboard: [post-critsmash-triage]
Whiteboard: [post-critsmash-triage] → [post-critsmash-triage][adv-main102-]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: