22

We found a severe problem with the interpretation of our Javascript code that only occurs on iOS 5/Safari 6 (then current iPad release) that we think is due to critical bug in the Just in Time JS compiler in Safari. (See updates below for more affected versions and versions that seem to now contain a fix).

We originally found the issue in our online demos of our library: the demos crash more or less randomly but this happens only the second time (or even later) that the same code is executed. I.e. if you run the part of the code once, everything works OK, however subsequent runs crash the application.

Interestingly executing the same code in Chrome for iOS the problem does not show, which we believe is due to the missing JIT capabilities of the Webview that is used in Chrome for iOS.

After a lot of fiddling we finally think we found at least one problematic piece of code:

  var a = 0; // counter for index
  for (var b = this.getStart(); b !== null; b = b.getNext()) // iterate over all cells
    b.$f = a++; // assign index to cell and then increment 

In essence this is a simple for loop that assigns each cell in a linked list data structure its index. The problem here is the post-increment operation in the loop body. The current count is assigned to the field and updated after the expression is evaluated, basically the same as first assigning a and then incrementing it by one.

This works OK in all browsers we tested and in Safari for the first couple of times, and then suddenly it seems as if the counter variable a is incremented first and then the result is assigned, like a pre-increment operation.

I have created a fiddle that shows the problem here: http://jsfiddle.net/yGuy/L6t5G/

Running the example on an iPad 2 with iOS 6 and all updates the result is OK for the first 2 runs in my case and in the third identic run suddenly the last element in the list has a value assigned that is off by one (the output when you click the "click me" button changes from "from 0 to 500" to "from 0 to 501")

Interestingly if you switch tabs, or wait a little it can happen that suddenly the results are correct for two or so more runs! It seems as if Safari sometimes resets is JIT caches.

So since I think it may take a very long for the Safari team to fix this bug (which I have not yet reported) and there may be other similar bugs like this lurking in the JIT that are equally hard to find, I would like to know whether there is a way to disable the JIT functionality in Safari. Of course this would slow down our code (which is very CPU intensive already), but better slow than crashing.

Update: Unsurprisingly it's not just the post increment operator that is affected, but also the post decrement operator. Less surprisingly and more worryingly is that it makes no difference if the value is assigned, so looking for an assignment in existing code is not enough. E.g. the following the code b.$f = (a++ % 2 == 0) ? 1 : 2; where the variables value is not assigned but just used for the ternary operator condition also "fails" in the sense that sometimes the wrong branch is chosen. Currently it looks as if the problem can only be avoided if the post operators are not used at all.

Update: The same issue does not only exist in iOS devices, but also on Mac OSX in Safari 6 and the latest Safari 5: These have been tested and found to be affected by the bug: Mac OS 10.7.4, Safari 5.1.7 Mac OS X 10.8.2, WebKit Nightly r132968: Safari 6.0.1 (8536.26.14, 537+). Interestingly these do not seem to be affected: iPad 2 (Mobile) Safari 5.1.7, and iPad 1 Mobile Safari 5.1. I have reported these problems to Apple but have not received any response, yet.

Update: The bug has been reported as Webkit bug 109036. Apple still has not responded to my bug report, all current (February 2013) Safari versions on iOS and MacOS are still affected by the problem.

Update 27th of February 2013: It seems the bug has been fixed by the Webkit team here! It was indeed a problem with the JIT and the post-operators! The comments indicate that more code might have been affected by the bug, so it could be that more mysterious Heisenbugs have been fixed, now!

Update October 2013: The fix finally made it into production code: iOS 7.0.2 at least on iPad2 does not seem to suffer from this bug anymore. I did not check all of the intermediate versions, though, since we worked around the problem a long time ago.

Sebastian
  • 7,729
  • 2
  • 37
  • 69
  • can you write the code to work around the bug? Perhaps `a += 1` instead of `a++`? – Matt Greer Oct 30 '12 at 20:29
  • 1
    Actually writing b.$f = a, a++; works around the problem, also initializing a to -1 and writing b.$f = ++a; worked, at least it did not crash anymore in my tests... The problem is that although we could fix that specific line of code there are probably dozens of similar in our codebase elsewhere, and also from the exceptions we were receiving there could be completely different pieces of code that fail in a similar fashion and might not be related to this specific "post-increment in loop" scheme. – Sebastian Oct 30 '12 at 20:30
  • 1
    It's only a guess but I remember some dynamic code (`eval` or variables changing type during runtime) can trip up JIT compiler so it falls back to interpreter. – Chris Hasiński Nov 05 '12 at 08:55
  • @KrzysztofHasiński Thanks, but I guess this needs to be done at least on a function level, i.e. each function would have to contain at least one such type of code construct so that the compiler would not kick in for that specific scope?! Or are you saying this switches off the JIT for the whole page? – Sebastian Nov 05 '12 at 08:59
  • Yes, I believe it is on the function level. One way to do it would be to load your code using `eval()` instead of loading it like usual when running in Safari. But please test it, I won't put it as an answer as I have no possibility now to actually check it and confirm that it can disable JIT. – Chris Hasiński Nov 05 '12 at 09:03
  • Also : have a look at this: http://stackoverflow.com/questions/9410533/can-the-firefox-javascript-jit-be-disabled-from-a-script It's firefox but the rules should be somewhat similar because of what can and cannot be compiled by JIT. – Chris Hasiński Nov 05 '12 at 09:04
  • @KrzysztofHasiński using eval and with in the script did not yield different results: http://jsfiddle.net/yGuy/L6t5G/10/ - of course this is a rather early and "broad" eval, however evaluating each line of code separately clearly is not a choice. – Sebastian Nov 05 '12 at 09:31
  • Considering the `eval` and `with` solutions do not work you might need to dive deeply into Webkit to find functions not yet available in JIT. This can be quite complicated and unstable (minor different versions might break your code). If I were you I would look into serving affected Safari versions JS with workaround included. Selenium with Safari hooked into it might be a good way to find all places with those errors (record on Fx, play on Safari). – Chris Hasiński Nov 05 '12 at 09:34
  • @KrzysztofHasiński Thanks. Of course finding a function not available in JIT could solve this issue - and finding such a function that reliably has this effect and does not need to be injected into everly function would be a valid answer to my question. – Sebastian Nov 05 '12 at 10:13
  • @KrzysztofHasiński RE the workaround: The problem is that I don't know all the places where to apply it - I could be looking for all post increment operations and replace them, however a quick check shows that the same problem exists with the post decrement operator and possibly many more variants. For now it's a problem with the execution order and post operators in general and finding the places is hard - "finding the error" can not be done automatically - I debugged for hours to find just one of them. Finding the root of data corruption is hard, even more if it is not deterministic. – Sebastian Nov 05 '12 at 10:19
  • 3
    Between this and the [POST bug](http://stackoverflow.com/q/12506897/201952), it's really starting to feel like Safari is the new IE. – josh3736 Nov 09 '12 at 16:29

3 Answers3

11

Try-catch blocks seem to disable the JIT compiler on Safari 6 on Lion for the part directly inside the try block (this code worked for me on Safari 6.0.1 7536.26.14 and OS X Lion).

// test function
utility.test = function(){
    try {
        var a = 0; // counter for index
        for (var b = this.getStart(); b !== null; b = b.getNext()) // iterate over all cells
            b.$f = a++; // assign index to cell and then increment
    }
    catch (e) { throw e }
    this.$f5 = !1; // random code
};

This is at least a documented behavior of the current version of Google's V8 (see the Google I/O presentation on V8), but I don't know for Safari.

If you want to disable it for the whole script, one solution would be to compile your JS to wrap every function's content inside a try-catch with a tool such as burrito.

Good job on making this reproducible!

guillaume
  • 1,380
  • 10
  • 15
  • This worked on the iPad, too (at least I was not able to reproduce the issue). I knew about the try-block and as I said in a comment I was looking for a way to more globally switch off the JIT. Thanks for the pointer to burrito, but since in our case we are generating the JS sources automatically, we could add the try block directly during that process, however I could also just rewrite post-operators to become two operations. It's just that I would rather have less intrusive changes than either of these solutions. So +1 for the nice answer, although I am not yet going to mark it as an answer. – Sebastian Nov 08 '12 at 10:50
  • Similarly disables JIT for Carakan (in Opera). – gsnedders Nov 11 '12 at 20:25
  • Although I was looking for a solution that is less intrusive, I'll accept the answer for now and you will get the bounty. I'll probably not going to use that solution, though, but will try to identify code like this automatically and refactor the code into separate instructions instead. Thanks anyway. – Sebastian Nov 12 '12 at 07:35
1

IMO, the correct solution is to report the bug to Apple, then workaround it in your code (surely using a separate a = a + 1; statement will work, unless the JIT is even worse than you thought!). It does indeed suck, though. Here's a list of common things you can also try throwing in to the function to make it de-optimise and not use JIT:

  • Exceptions
  • 'with' statement
  • using arguments object, e.g. arguments.callee
  • eval()

The problem with those is if the Javascript engine is optimised to JIT them before they fix that bug, in which case you're back to crashing. So, report and workaround!

AshleysBrain
  • 22,335
  • 15
  • 88
  • 124
  • 2
    As I already said, I am probably going to use the separate instructions approach, which is not as trivial as it may look initially, because this can be inside ternary operator branches, etc. Of course I have submitted the bug (ID 12606761) 12 days ago, but they did not yet react in any way to my report. How can I raise the attention to a bug in the apple bug database? – Sebastian Nov 12 '12 at 07:41
1

Actually, the FOR loop bug is still present in Safari on iOS 7.0.4 in iPhone 4 and iPad 2. The loop failing can be significantly simpler than the illustration above, and it rakes several passes through the code to hit. Changing to a WHILE loop allows proper execution.

Failing code:

function zf(num,digs) 
{ 
var out = ""; 
var n = Math.abs(num); 
for (digs;  digs>0||n>0; digs--)
{ 
    out = n%10 + out; 
    n = Math.floor(n/10); 
}  
return num<0?"-"+out:out; 
} 

Successful code:

function zf(num,digs) 
{ 
var out = ""; 
var n = Math.abs(num); 
do 
{ 
    out = n%10 + out; 
    n = Math.floor(n/10); 
} 
while (--digs>0||n>0) 
return num<0?"-"+out:out; 
} 
Bob Riess
  • 11
  • 1