mozilla

Firefox Bug 784402, Pointer Lock must respect iframe sandbox flag

Recently I’ve worked on the Firefox Bug 784402 – Pointer Lock must respect iframe sandbox flag.

This is a quick overview of what had to be done on the bug.

Sandbox flags

First lets check what the sandbox attribute does:
A quote from the w3c spec

The sandbox attribute, when specified, enables a set of extra restrictions on any content hosted by the iframe. Its value must be an unordered set of unique space-separated tokens that are ASCII case-insensitive. The allowed values are allow-forms, allow-popups, allow-same-origin, allow-scripts, and allow-top-navigation. When the attribute is set, the content is treated as being from a unique origin, forms and scripts are disabled, links are prevented from targeting other browsing contexts, and plugins are secured. The allow-same-origin keyword allows the content to be treated as being from the same origin instead of forcing it into a unique origin, the allow-top-navigation keyword allows the content to navigate its top-level browsing context, and the allow-forms, allow-popups and allow-scripts keywords re-enable forms, popups, and scripts respectively.

With pointerlock landing on Firefox 15, it was decided that a new sandbox flag should be created to restrict the pointerlock usage on embedded scripts in a page, so for example: if you add an advertisement script on your page, you don’t want to give the permissions to the advertisement to lock the pointer to itself.
To manage that, the allow-pointer-lock sandbox was created.

An overview of how the sandbox flags work:
List of flags:

 
 /**  
 * This flag prevents content from navigating browsing contexts other than  
 * the sandboxed browsing context itself (or browsing contexts further  
 * nested inside it), and the top-level browsing context.  
 */  
 const unsigned long SANDBOXED_NAVIGATION = 0x1;

/**  
 * This flag prevents content from navigating their top-level browsing  
 * context.  
 */  
 const unsigned long SANDBOXED_TOPLEVEL_NAVIGATION = 0x2;

/**  
 * This flag prevents content from instantiating plugins, whether using the  
 * embed element, the object element, the applet element, or through  
 * navigation of a nested browsing context, unless those plugins can be  
 * secured.  
 */  
 const unsigned long SANDBOXED_PLUGINS = 0x4;

/**  
 * This flag forces content into a unique origin, thus preventing it from  
 * accessing other content from the same origin.  
 * This flag also prevents script from reading from or writing to the  
 * document.cookie IDL attribute, and blocks access to localStorage.  
 */  
 const unsigned long SANDBOXED_ORIGIN = 0x8;

/**  
 * This flag blocks form submission.  
 */  
 const unsigned long SANDBOXED_FORMS = 0x10;

/**  
 * This flag blocks script execution.  
 */  
 const unsigned long SANDBOXED_SCRIPTS = 0x20;

/**  
 * This flag blocks features that trigger automatically, such as  
 * automatically playing a video or automatically focusing a form control.  
 */  
 const unsigned long SANDBOXED_AUTOMATIC_FEATURES = 0x40;

/**  
 * This flag blocks the document from acquiring pointerlock.  
 */  
 const unsigned long SANDBOXED_POINTER_LOCK = 0x80;  

Parsing the flags

So we have a 32 bit integer to store the sandbox flags.

Breaking down the integer we have 8 bytes
We can represent each byte in hexadecimal format:

So the number 0xFFFFFFFF has all the bits turned ON

Knowing that, we could use each bit of the integer to represent a flag.
We don’t care about the decimal value of that integer, since we are using it to store flags and not values.
So by saying 0x1, we are telling to turn the first bit of the first byte on, 0x2 turns the second bit of the first byte on
0x10 on the other hand tells to turn the first bit of the second byte on.
Remember that we are using hexadecimal notation.

So in the end, what’s happening is that each flag is turning a different bit on the integer

Later we’ll be able to check if that specific bit is ON or OFF and determine the status of the flag.

One thing to keep in mind is that if the iframe doesn’t have the sandbox attribute, then all the flags are turned OFF by default.

 
 <iframe></iframe>  

If the iframe has an empty sandbox attribute, then all the flags are ON by default

 
 <iframe sandbox=""></iframe>  

To turn the flags off, you can specify the feature you want to enable in the sandbox attribute:

   
 <iframe sandbox="allow-pointer-lock allow-same-origin"></iframe>  

In the snippet above both the allow-pointer-lock and allow-same-origin flag would be turned OFF, all the other flags would be ON

This is the code that parses the sandbox flags:

   
 /**  
 * A helper function that parses a sandbox attribute (of an <iframe> or
 * a CSP directive) and converts it to the set of flags used internally.
 *
 * @param aAttribute the value of the sandbox attribute
 * @return the set of flags
 */
uint32_t
nsContentUtils::ParseSandboxAttributeToFlags(const nsAString & aSandboxAttrValue) {
  // If there’s a sandbox attribute at all (and there is if this is being  
  // called), start off by setting all the restriction flags.  
  uint32_t out = SANDBOXED_NAVIGATION |
    SANDBOXED_TOPLEVEL_NAVIGATION |
    SANDBOXED_PLUGINS |
    SANDBOXED_ORIGIN |
    SANDBOXED_FORMS |
    SANDBOXED_SCRIPTS |
    SANDBOXED_AUTOMATIC_FEATURES |
    SANDBOXED_POINTER_LOCK;

  if (!aSandboxAttrValue.IsEmpty()) {
    // The separator optional flag is used because the HTML5 spec says any  
    // whitespace is ok as a separator, which is what this does.  
    HTMLSplitOnSpacesTokenizer tokenizer(aSandboxAttrValue, ‘‘,
      nsCharSeparatedTokenizerTemplate < nsContentUtils::IsHTMLWhitespace > ::SEPARATOR_OPTIONAL);

    while (tokenizer.hasMoreTokens()) {
      nsDependentSubstring token = tokenizer.nextToken();
      if (token.LowerCaseEqualsLiteral("allow-same-origin")) {
        out &= ~SANDBOXED_ORIGIN;
      } else if (token.LowerCaseEqualsLiteral("allow-forms")) {
        out &= ~SANDBOXED_FORMS;
      } else if (token.LowerCaseEqualsLiteral("allow-scripts")) {
        // allow-scripts removes both SANDBOXED_SCRIPTS and  
        // SANDBOXED_AUTOMATIC_FEATURES.  
        out &= ~SANDBOXED_SCRIPTS;
        out &= ~SANDBOXED_AUTOMATIC_FEATURES;
      } else if (token.LowerCaseEqualsLiteral("allow-top-navigation")) {
        out &= ~SANDBOXED_TOPLEVEL_NAVIGATION;
      } else if (token.LowerCaseEqualsLiteral("allow-pointer-lock")) {
        out &= ~SANDBOXED_POINTER_LOCK;
      }
    }
  }

  return out;
}

First all the flags are turned ON.
Then it checks if the sandbox attribute has any values, if it does it splits them and compares against the possible flags.
Once it finds a match, it does a BIT NEGATION on the flag and a BIT AND with the integer that has all the other flags.
What happens is that the flag being parsed is turned OFF.

In the end the integer with the status of all the flags is returned.

Locking the pointer

Now lets take a look at the code that checks for the allow-pointer-lock flag when an element requests pointerlock

 
 bool  
 nsDocument::ShouldLockPointer(Element* aElement)  
 {  
 // Check if pointer lock pref is enabled  
 if (!Preferences::GetBool("full-screen-api.pointer-lock.enabled")) {  
 NS_WARNING("ShouldLockPointer(): Pointer Lock pref not enabled");  
 return false;  
 }

 if (aElement != GetFullScreenElement()) {  
 NS_WARNING("ShouldLockPointer(): Element not in fullscreen");  
 return false;  
 }

 if (!aElement->IsInDoc()) {  
 NS_WARNING("ShouldLockPointer(): Element without Document");  
 return false;  
 }

 if (mSandboxFlags & SANDBOXED_POINTER_LOCK) {  
 NS_WARNING("ShouldLockPointer(): Document is sandboxed and doesn’t allow pointer-lock");  
 return false;  
 }

 // Check if the element is in a document with a docshell.  
 nsCOMPtr ownerDoc = aElement->OwnerDoc();  
 if (!ownerDoc) {  
 return false;  
 }  
 if (!nsCOMPtr(ownerDoc->GetContainer())) {  
 return false;  
 }  
 nsCOMPtr ownerWindow = ownerDoc->GetWindow();  
 if (!ownerWindow) {  
 return false;  
 }  
 nsCOMPtr ownerInnerWindow = ownerDoc->GetInnerWindow();  
 if (!ownerInnerWindow) {  
 return false;  
 }  
 if (ownerWindow->GetCurrentInnerWindow() != ownerInnerWindow) {  
 return false;  
 }

 return true;  
 }  

The ShouldLockPointer method is called every time an element requests pointerlock, the method does some sanity checks and makes sure everything is correct.
To check for the allow-pointer-lock sandbox flag, a BIT AND with the mSandBoxFlags and the SANDBOXPOINTERLOCK const is performed, we’ve looked at the SANDBOXPOINTERLOCK flag before, it has the value of 0x80
So if pointerlock is allowed, the mSandboxFlags would have the SANDBOXPOINTERLOCK flag OFF and the BIT AND would be false.

A big thanks to Ian Melven.
Ian is the one who implemented the sandbox attribute on Firefox and gave me some guidance on the PointerLock sandbox attribute bug.

Bug 728893 - Allow mochitest iframe to go fullscreen

Bug 728893 was encountered while implementing PointerLock.

A Quick Overview on Mochitests

By default an iframe is not allowed to go fullscreen mode, and since all the mochitests are run inside and iframe, if one of the mochitest needed to go fullscreen it would not work (pointerlock for example)

There are two different ways to run mochitests. With runtests.py or mochitest-plain(ends up running runtests.py)

  1. python ./obj-dir/_tests/testing/mochitest/runtest.py –test-path=dom/tests/mochitest/pointerlock
  2. TEST_PATH=/dom/test/mochitest/pointerlock make -C obj-dir/ mochitest-plain

The first one calls explicit the runtests.py, that is the script that creates the http server
The second one will run mochitest-plain.
Now what is mochitest-plain?
First we need to look at the root’s Makefile
/Makefile.in
Line 129 is including testsuite-targets.mk, that’s where mochitest-plain is defined

127 ifdef ENABLE_TESTS
128 # Additional makefile targets to call automated test suites
129 include $(topsrcdir)/testing/testsuite-targets.mk
130 endif

mochitest-plain
Here is where mochitest-plain is defined, it calls $(RUN_MOCHITEST)

128 mochitest-plain:
129 $(RUNMOCHITEST)
130 $(CHECK
TEST_ERROR)

RUN_MOCHITEST
In the end runtests.py is ran with some default flags

> 64 RUNMOCHITEST =
65 rm -f ./$@.log &&
66 $(PYTHON) _tests/testing/mochitest/runtests.py –autorun –close-when-done
67 –console-level=INFO –log-file=./$@.log –file-level=INFO
68 –failure-file=$(call core
abspath,tests/testing/mochitest/makefailures.json)
69 $(SYMBOLS
PATH) $(TESTPATHARG) $(EXTRATESTARGS)

Running the tests with mochitest-plain will add some flags to the execution of runtest.py

[diogogmt@rome mozilla-central-diogogmt]$ TESTPATH=dom/tests/mochitest/pointerlock/ make -C ff-dbg/ mochitest-plain
make: Entering directory `/home/diogogmt/mozilla-central-diogogmt/ff-dbg’
rm -f ./mochitest-plain.log && /usr/bin/python2.7 _tests/testing/mochitest/runtests.py –autorun **–close-when-done**–console-level=INFO –log-file=./mochitest-plain.log –file-level=INFO –failure-file=/home/diogogmt/mozilla-central-diogogmt/ff-dbg/
tests/testing/mochitest/makefailures.json –symbols-path=./dist/crashreporter-symbols –test-path=dom/tests/mochitest/pointerlock/

The flag –close-when-done is set by default. However sometimes you may want to leave the browser window open for debug purposes. I’m not sure if there is a way to tell mochitest-plain not run the runtests.py with the –close-when-done flag not set. What I usually do is run the tests calling directly runtest.py, since it runs withouth any flags, giving me more flexbility.

When not specifying a file, the mochitest will load all the files of the dir and run them inside the harness

For example:
dom/tests/mochitest/Makefile.in
The mochitest Makefile contains the list of the dirs of some tests

45 DIRS +=
46 dom-level0
47 dom-level1-core
48 dom-level2-core
49 dom-level2-html
50 ajax
51 bugs
52 chrome
53 general
54 whatwg
55 geolocation
56 localstorage
57 orientation
58 sessionstorage
59 storageevent
60 $(NULL

dom/test/mochitest/geolocation/Makefile.in
In the geolocation Makefile all the tests for that module are listed

41 relativesrcdir = dom/tests/mochitest/geolocation
42
43 include $(DEPTH)/config/autoconf.mk
44
45 include $(topsrcdir)/config/rules.mk
46
47 TESTFILES =
48 testallowCurrent.html
49 test
allowWatch.html
50 testcancelCurrent.html
51 test
cancelWatch.html
52 testclearWatch.html
53 test
clearWatchinvalid.html
54 test
manyCurrentConcurrent.html
55 testmanyCurrentSerial.html
56 test
manyWatchConcurrent.html
57 testmanyWatchSerial.html
58 test
manyWindows.html
59 testoptionalapiparams.html
60 test
shutdown.html
61 testwindowClose.html
62 test
timerRestartWatch.html
63 testworseAccuracyDoesNotBlockCallback.html
64 geolocation.html
65 geolocation
common.js
66 networkgeolocation.sjs
67 windowTest.html
68 $(NULL)
69
70 libs:: $(
TESTFILES)
71 $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/
tests/testing/mochitest/tests/$(relativesrcdir)

From line 48 to 67 are listed all the test files
Line 41 is defined the path where the files are located
Line 71 is the path where the files will be copied after compiling firefox, for example:
/obj-dir/_tests/testing/mochitest/tests/dom/tests/mochitest/geolocation

Running single tests
When running the test as a stand alone, there is no need for an iframe, the test is loaded in the parent window, so the element is allowed to go fullscreen, different from when the tests are run inside the harness.

The Problem

When we first started writing mochitests for pointerlock we had to find a way to run the tests inside the harness while setting them to fullscreen.
The solution was to use the same approach as the fullscreen tests. To create a harness inside the harness, for example

Solution

The solution was simple, we just had to add the attribute mozallowfullscreen=true to the mochitest iframe.
However, there are three places where the iframe gets defined:

server.js is used for mochitest-plain
browser_harness.xul is used for mochitest-chrome and mochitest-browser-chrome
plain-loop remains a mystery :P

For server.js:

   
 IFRAME({scrolling: "no", id: "testframe", width: "500", height: "300", mozallowfullscreen: "true"})  

For browser_harness.xul

  
 <iframe id="testframe" scrolling="no" width="550" height="350"></iframe>  

You can find the complete patch here

Sanity tests

To make sure the mochitest framework works, before running all the tests a list of sanity tests is executed to test the framework itself.
You can see the list of the tests in the /testing/mochitest/tests/Makefile.in

43 relativesrcdir = testing/mochitest/tests
44
45 include $(DEPTH)/config/autoconf.mk
46
47 PARALLELDIRS =
48 MochiKit-1.4.2
49 SimpleTest
50 browser
51 $(NULL)
52
53 include $(topsrcdir)/config/rules.mk
54
55 _TEST
FILES =
56 testsanity.html
57 test
sanityException.html
58 testsanityException2.html
59 test
sanityWindowSnapshot.html
60 testSpecialPowersExtension.html
61 test
SpecialPowersExtension2.html
62 fileSpecialPowersFrame1.html
63 $(NULL)
64
65 ifneq ($(OS
TARGET),Android)
66 # Disabled on Android for permaorange, see bug 688052
67 TESTFILES +=
68 testsanityEventUtils.html
69 test
sanitySimpletest.html
70 endif
71 # Copy the sanity tests into a subdirectory, so the top level is all dirs
72 # in the test screen.
73 libs:: $(TESTFILES)
74 $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/tests/$(relativesrcdir)/Harnesssanity

One thing to notice is the folder the sanity tests are copied to: Harness_sanity

After Firefox is compiled, all the tests are copied to /obj-dir/_tests/testing/mochitest/tests
So to run the geolocation tests for example:

  • TEST_PATH=dom/tests/mochitest/geolocation

The Harness_sanity on the other hand:

  • TESTPATH=Harnesssanity

Since allowing the iframe to go fullscreen mode could break other tests we decided to add a check on SimpleTest.finish() to cancel fullscreen mode if the test forgets to cancel it

  
 + // Cancel element fullscreen mode due to Bug 728893  
 + if (document && document.mozFullScreenElement) {  
 + document.mozCancelFullScreen();  
 + }  

So now we needed a test to make sure SimpleTest.finish was actually cancelling fullscreen.
My first thought was to have two test files, testfullscreen1.html and testfullscreen2.html. On testfullscreen1.html the element would be set to fullscreen mode and without cancelling call SimpleTest.finish(), then on testfullscreen2.html would check the fullscreen status of the iframe, it should be false since SimpleTest.finish() supposed to cancel it.
However, there was an easier way to write the test, with just one file instead of two.

  

 +  

 +  
 + /** Test for Bug 728893  
 + Checks if SimpleTest.finish cancels fullscreen mode  
 + The assertion happens after calling SimpleTest.finish  
 + Running the test without the harness won’t work  
 + **/  
 +  
 + SpecialPowers.setBoolPref("full-screen-api.enabled", true);  
 + SpecialPowers.setBoolPref("full-screen-api.allow-trusted-requests-only",  
 + false);  
 +  
 + SimpleTest.waitForExplicitFinish();  
 +  
 + var div = document.getElementById("div");  
 +  
 + document.addEventListener("mozfullscreenchange", function (e) {  
 + if (document.mozFullScreenElement === div) {  
 + SimpleTest.finish();  
 + }  
 + else {  
 + is(false, document.mozFullScreen,  
 + "SimpleTest.finish should cancel fullscreen mode");  
 + }  
 + }, false);  
 +  
 + function start() {  
 + SimpleTest.waitForFocus(function() {  
 + div.mozRequestFullScreen();  
 + });  
 + }  
 +  
 +  

 +  
 +  
 +  

The assertion “is(false, document.mozFullScreen, “SimpleTest.finish should cancel fullscreen mode”);” is happening after SimpleTest.finish is called, the reason that it is possible to still make assertions after calling SimpleTest.finish is because the test itself is being run inside the mochitest harness

SimpleTest.js

 
 678 if (parentRunner) {  
 679 /* We’re running in an iframe, and the parent has a TestRunner */  
 680 parentRunner.testFinished(SimpleTest._tests);  
 681 } else {  
 682 SimpleTest.showReport();  
 683 }  

Line 678 checks if the test is being run inside a harness, if true it goes to the next test, but it doesn’t terminates the current one.
Different from line 682 that call showReport, terminating the current test and displaying the results.

**Big thans to Clint(ctalbert) for all the help with this bug

PointerLock API Updates

A quick update on the Firefox PointerLock API implementation

Lets start with mochitests. While writing mochitests for pointerlock we stumbled on two problems

  1. Not being able to specify how many tests should run (different platforms were running different number of tests)
  2. Mochitest iframe not allowed to go fullscreen, making us run all the tests on a different window

David Humphrey came up with a solution for our first problem and added an “expect” functionality to the mochitest framework.
So now we can specify how many tests should occur when making asynchronous tests, for example:
SimpleTest.waitForExplicitFinish(3)
Bug 724578

For our second problem, I added the attribute mozallowfullscreen=true to the mochitest iframe that runs all tests.
I’m not sure if there was a specific reason for not allowing fullscreen on the mochitest iframe, but if it wasn’t it will simplify a lot writing tests for pointerlock
Bug 728893

Spec Updates

The spec had two major changes

  1. Switching from callbacks to events
  2. Moving functionality to the Document and Element

For example:
Everytime the pointer is locked/unlocked a mozpointerchange event will be dispatched to the document
A mozpointererror event will be dispatched if there are any errors while locking the pointer
Now It’s possible to access the element with the pointer locked via the document

  
 var div = document.createElement("div");

document.addEventListener("mozpointerlockchange", function (e) {  
 if (document.mozPointerLockElement === div) {  
 // Pointer is locked  
 }  
 }, false);

document.addEventListener("mozpointerlockerror", function (e) {

}, false);

document.addEventListener("mozfullscreenchange", function (e) {  
 if (document.mozFullScreen &&  
 document.mozFullScreenElement === div) {  
 div.mozRequestPointerLock();  
 }  
 }, false);

div.mozRequestFullScreen();

Instead of something like this:

 
 var div = document.createElement("div");

div.addEventListener("mozpointerlocklost", function (e) {  
 // Dispatched when pointer is unlocked  
 }, false);

document.addEventListener("mozfullscreenchange", function (e) {  
 if (document.mozFullScreen &&  
 document.mozFullScreenElement === div) {  
 navigator.mozPointer.lock(  
 div, // Element  
 function () {  
 // Success callback  
 },  
 function () {  
 // Failure callback  
 }  
 );  
 }  
 }, false);

div.mozRequestFullScreen();

Updating PointerLock API - Callbacks, Events and Threads

The PointerLock implementation of Firefox is going great, we are close to having the patch ready to land, maybe Firefox 13.

The work being done now is mainly some final touches, specially on the mochitests and on the API.

Recently the W3C PointerLock spec has been updated, the changes are the following:

  • When locking the mouse, dispatch pointerlockchange/pointerlockerror events instead of firing callbacks
  • Locking the pointer by requesting pointer lock on the target element
  • Adding a reference to the locked element in the Document
  • Exiting pointerlock by calling exitPointerLock on the Document

Those were significant changes, since it affected a big chunk of the code we had it implemented. However, I believe these updates to the API are beneficial, since with them developers will have an API similar to the fullscreen to work with.

The first bit I started working on was to dispatch the pointerlockchange/pointerlockerror instead of callbacks.

To Dispatch the events, the nsAsyncDOMEvent object was used:

   
 static void  
 DispatchPointerLockChange(nsINode* aTarget)  
 {  
 nsRefPtr e =  
 new nsAsyncDOMEvent(aTarget,  
 NS_LITERAL_STRING("mozpointerlockchange"),  
 true,  
 false);  
 e->PostDOMEvent();  
 }  

Same logic to dispatch the pointerlockerror and pointerlocklost

One of the good things about having to go back and rewrite some code, is the fact that opens the possibility to analyse some of the decisions made before.
Specifically in this case, the use of different threads when locking the pointer.
At first, the callbacks were being fired on a different thread so the execution wouldn’t hang, and the Lock method would be able to return as soon as possible and not make the user wait for a result.

Before, the logic for callbacks was mainly based off the nsGeoLocation implementation. However, now with the pointerlock API looking more like the fullscreen api I went and looked how they handle setting the element into fullscreen.
I had written a blog post a while back inspecting the fullscreen API, so even with the API receiving some changes it was easy to locate the code path for requesting fullscreen on an element.

Here is a simple diagram I drew

The diagram shows that once mozRequestFullScreen is called on an element, the method returns really fast and all the heavy processing happens on a separate thread.

On the other hand, this is how PointerLock does it:

On PointerLock, different from the FullScreen, the heavy processing happens on the main thread, and the new thread only handles the callback firing. Now switching to events, even less processing happens on the new thread, so that made me rethink the logic for locking the pointer.

I remember hearing that all the code that involves changing the presentation, it needs to happen on the main thread, so maybe that’s why we’re not spinning the pointerlock check/validation to another thread, since it involves changing the UI presentation by hiding the pointer if the lock is successful.

Another thing that caught my attention was the fact that on the fullscreen code, the nsCallRequestFullScreen object was dispatched to a new thread using NSDispatchToCurrentThread and on PointerLock we are using NSDispatchToMainThread


NSDispatchToCurrentThread
NSGetCurrentThread
nsThreadManager::GetCurrentThread NS_DispatchToMainThread
mMainThread

Bug 713608 Update

One of the bugs I’m currently working on is: Bug 713608 – HTML5 Video controls are missing in Fullscreen
A quick overview of the bug:
This bug involves working with html5 video controls.
The goal of the bug is to enable the stock controls when the video enters in fullscreen via the context menu. For example: the controls are hidden when the video isn’t in fullscreen, then the user right clicks on the video and selects enter fullscreen. When the video switches to fullscreen mode, the controls will be displayed, even though they weren’t before toggling fullscreen, and after exiting fullscreen mode, if the controls were disabled in the first place, disable them again.

After working on Bug 714071 – The Show Statistics setting is not preserved when toggling the full screen that also involved working on the videocontrols.xml file, I thought would be good to keep digging how XBL bindings work on firefox.
So far Bug 713608 has proven to be more complicated than I thought it would be.
At the beginning, I followed the same logic used on Bug 714071 and tried to find the method that shows/hides the controls. However, different from the Statistics menu option, it seems that hiding the controls uses a different approach.
This is the code that displays the statistics of a video

   
 showStatistics : function(shouldShow) {  
 if (this.statsInterval) {  
 clearInterval(this.statsInterval);  
 this.statsInterval = null;  
 }

 if (shouldShow) {  
 this.video.mozMediaStatisticsShowing = true;

 this.statsOverlay.hidden = false;  
 this.statsInterval = setInterval(this.updateStats.bind(this), this.STATS_INTERVAL_MS);  
 this.updateStats();  
 } else {  
 delete this.video.mozMediaStatisticsShowing;  
 this.statsOverlay.hidden = true;  
 }  
 },  

But I couldn’t find the equivalent method for the video controls.

Browsing through the code, I was able to find the methods triggered by the event listeners for mousemove/over/out used by the video element:

MouseMove

   
 onMouseMove : function (event) {  
 // If the controls are static, don’t change anything.  
 if (!this.dynamicControls)  
 return;

 clearTimeout(this._hideControlsTimeout);

 // Suppress fading out the controls until the video has rendered  
 // its first frame. But since autoplay videos start off with no  
 // controls, let them fade-out so the controls don’t get stuck on.  
 if (!this.firstFrameShown &&  
 !(this.video.autoplay && this.video.mozAutoplayEnabled))  
 return;

 this.startFade(this.controlBar, true);  
 // Hide the controls if the mouse cursor is left on top of the video  
 // but above the control bar and if the click-to-play overlay is hidden.  
 if (event.clientY < this.controlBar.getBoundingClientRect().top && this.clickToPlay.hidden) {  
 this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);  
 }  
 },  

MouseInOut

   
 onMouseInOut : function (event) {  
 // If the controls are static, don’t change anything.  
 if (!this.dynamicControls)  
 return;

 clearTimeout(this._hideControlsTimeout);

 // Ignore events caused by transitions between child nodes.  
 // Note that the videocontrols element is the same  
 // size as the *content area* of the video element,  
 // but this is not the same as the video element’s  
 // border area if the video has border or padding.  
 if (this.isEventWithin(event, this.videocontrols))  
 return;

 var isMouseOver = (event.type == "mouseover");

 // Suppress fading out the controls until the video has rendered  
 // its first frame. But since autoplay videos start off with no  
 // controls, let them fade-out so the controls don’t get stuck on.  
 if (!this.firstFrameShown && !isMouseOver &&  
 !(this.video.autoplay && this.video.mozAutoplayEnabled))  
 return;

 if (!isMouseOver) {  
 this.adjustControlSize();

 // Keep the controls visible if the click-to-play is visible.  
 if (!this.clickToPlay.hidden)  
 return;

 // Setting a timer here to handle the case where the mouse leaves  
 // the video from hovering over the controls.  
 this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);  
 }  
 },  

My plan was to find the event listeners that trigger the display of the video controls, then find how those events are added/removed to element.

I found where the listeners were defined

   
   
   
 if (!this.isTouchControl)  
 this.Utils.onMouseInOut(event);  
   
   
 if (!this.isTouchControl)  
 this.Utils.onMouseInOut(event);  
   
   
 if (!this.isTouchControl)  
 this.Utils.onMouseMove(event);  
   
   

However, I couldn’t find where they are added/removed from the video element.

I feel that I’m getting close to finding a solution.

Big thanks to Jared Wein (jaws) for helping me with this and other bugs :)