The setup

Eleven days ago, Jon Lamendola was asking Adam Baldwin in our team chat how to do something with JavaScript. The discussion went like this:

A quick aside for any non devs, the longer version of what Baldwin could have written was:

if (b==1 && b===2 && b===herpderp) { // do stuff here }

This is an if-statement in JavaScript. JavaScript checks the “condition,” which is the code between the first set of ( parenthesis ), and if, and only-if it is true, runs the code between the { curly braces }.

The == / === are like an equals sign in math, they check if b is equal to 1, b is equal to 2, and b is equal to herpderp.

b and herpderp are just placeholders for other values, they could be anything, depending on what code was written before these lines.

Finally && means “and.” So in this case, for the condition to be true, b must equal 1 AND b must equal 2 AND b must equal herpderp.

At first glance, what Jon said seems to be true. Whatever b is, it can’t be both equal to 1 and 2 at the same time, so the condition can never be true, and the code inside { } will never execute.

However, it is at this moment I realise I am well and truly screwed. Jon has used two words which always get me: “can’t” and “never.” Something in the back of my mind niggles: what he said is basically true, but I am also pretty sure there’s a way that someone could break JavaScript subtly enough to make it not true. And I know it’s going to bother me until I can figure it out. This leads to my single contribution to the conversation:

Phil: Oh man, you’ve totally nerdsniped me.

If you haven’t heard the term nerdsniping before, this xkcd comic lays it out pretty well:

Attempt one, == vs ===

I immediately pull up a text editor and start to experiment with how I can possibly get something to equal one and two at the same time.

The first thing my brain latches onto is the possibility of exploiting a typo in Baldwin’s code. He wrote b==1 && b===2 (note 2 = vs 3 =). This is still valid JavaScript, and they both mean roughly the same thing, but == is subtly different from === . Roughly, === checks if two things are the same thing, while == checks if two things are “equivalent.”

Checking if something is exactly equal to 2 is easy, first we check if something is a number, and if it is we check if it’s the number 2. If so, then yes something === 2. If it's not a number then it can't possibly be exactly equal to 2, no matter what it is.

But checking if something is “equivalent” to 1 is more tricky. If something is a number then we just check if it is the number 1. But if it’s not a number, it still might be “equivalent” to 1. To figure it out if something is equivalent to 1, JavaScript calls the valueOf() method on something, and if that returns 1, then something == 1.

So I did some experiments, here are just a few of the more interesting things I tried:

I started by verifying that valueOf works like I expected for == comparisons: If I start with a b that is just a random thing, I can change it’s valueOf function, and have it return 1, which works. var b = { } ; b . valueOf = function ( ) { return 1 ; } ; if ( b == 1 ) { console . log ( "This code is executed" ) ; }

I then think, ah ha, I can just start with a 2, and change it’s valueOf function to return 1, but that doesn’t seem to work :( var b = 2; b.valueOf = function () { return 1; }; if (b==1) { console.log('This aint called'); }

Then I remember that there are two slightly different ways to declare a number in JavaScript: var b = 1 is different to var b = new Number(1) . Ah ha! Maybe I change the prototype? And it works. Welp, I’ve just made a number 2, that is equivalent to 1. Way to break things! But breaking things is what I’m trying to do, so I guess that’s a win. Number.prototype.valueOf = function () { return 1; }; var b = new Number(2); if (b==1) { console.log('This is called'); //Woo! }

Alas now, b===2 is no longer true. I guess we’ve messed with the prototype too much, and JavaScript isn’t really convinced it’s still a proper number anymore.

Attempt two, read the specs

A few days pass. And the problem comes back to mind. I start digging in even further to the difference between == and === to see how I might be able to exploit it. A question on stackoverflow.com discussing the differences between the two, links to the original specification pdf document for JavaScript. Yes, thanks Jon, at this point I am now reading the ecmascript spec in pursuit of pedantry.

What’s possibly amusing is I’ve been a JavaScript developer for a number of years, and this is the first time I’ve ever even opened the spec document.

I read 11.9.3 The Abstract Equality Comparison Algorithm

I read 11.9.6 The Strict Equality Comparison Algorithm

I read 9.3 ToNumber and 9.3.1 ToNumber Applied to the String Type as they were mentioned by the Abstract Equality Comparison Algorithm.

I read those, and spend another hour or so hacking and experimenting, but alas, nothing of interest is revealed. To make b===2 b has to be a number, but to make b==1 I have to modify b's valueOf method, which immediately makes it not a proper number anymore.

Frustrated I give it up for the day. I’m still pretty sure it’s doable, but I just haven’t seen how yet.

11 days later... SUCCESS!

Eleven days after the original post, and I’ve all but forgotten about my little nerdsnipe failure.

I'm sitting reading a post by Reginald Braithwaite. I always enjoy Reg’s writing because he likes to break JavaScript down to its components and think about how and why it works. And somehow reading this post, and thinking about Reg, reminds me of my little problem.

I fire up a text editor again. For some reason the answer, this time, is obvious immediately.

var b = {}; var herpderp = 2; b.valueOf = function () { b = 2; return 1; }; if (b==1 && b===2 && b===herpderp) { console.log('This code runs!!!'); }

I run it, it works, success! So how does it work? Well first of all I had to realise the one little mistake in Jon’s phrasing. He said “b can't be 1, 2 and herpderp at the same time, so that code would never execute.” But that’s not quite what’s happening here. It’s not happening “at the same time”: first JavaScript checks if b==1 , then it checks if b===2 and then it checks if b===herpderp .

First we make b something that is not a number, but give it a valueOf function that returns 1, such that b==1 calls the valueOf function, sees that it got a 1 back, and this passes the first test. But we also did something sneaky in that valueOf function call, we changed b to be the number 2. Now when JavaScript gets to the next check, b===2 , it sees that b is 2, and that passes. Finally it gets to b===herpderp , which is easy as we can make herpderp whatever we like, so if we make it 2, we're all good and all three tests pass.

It should be noted, that I got very lucky. If Baldwin hadn’t missed that third = off the first check, or even if he’d put the two-= in one of the other checks instead, this solution wouldn’t have worked.

And there we have it

Eleven days later, and that little nerdsnipe can slip from my mind. Have I learned anything directly useful? Probably not. Have I learned something that might come in handy one day? Just maybe I’ll have a handy little tool I can use to pick away at some particularly fiddly bug. Or maybe, my next nerdsnipe will be a little easier to solve. At the very least I now know where the ecmascript spec document is!

After realising it had taken me 11 days to solve this nerdsnipe, I threw it out to twitter and got a hint from @jaz303 about another way of solving this that would have worked even if Baldwin hadn’t typo’d that first ===:

var n = 1 ; var herpderp = 3 ; Object . defineProperty ( global , 'b' , { get : function ( ) { return n ++ ; } } ) ; if ( b === 1 && b === 2 && b === herpderp ) { console . log ( 'this runs' ) ; }

Here, instead of creating a local variable b with var b , we are defining a property on the global object (in the browser this would be window ). The way that JavaScript looks up variable references, means that if it can’t find a local variable named b it will try it on the global object: so b===1 is the same as global.b===1 .