Homepage

# Fixing a baffling issue when running Electron as root in GNU/Linux

As mentioned in a previous post, I’m working on Etcher, an Electron application to burn IMG/ISO images to removable drives in all major operating systems. Since the application needs to get write permissions over devices, we present a nice “elevation” dialog at the start of the application to run the application as root.

This works great in OS X and Windows, however we faced the following error on some GNU/Linux systems:

Uncaught Error: Cannot find module '/home/jviotti/Projects/etcher/node_modules/electron-prebuilt/dist/resources/atom.asar/renderer/lib/init.js'


This file is in charge of exposing Electron’s public APIs, exporting Node.js bindings to global, and generally initialising the renderer context. Failing to load this file means that we can’t require() any modules nor do anything meaningful, leading the app to a broken state.

Notice the bug only manifests itself when running an Electron application on GNU/Linux as root. Running it with normal privileges works flawlessly.

We can prove the file exists by using the asar command line utility tool:

asar list resources/atom.asar | grep /renderer/lib/init.js /renderer/lib/init.js


After a debugging session with the Electron codebase, the root of the error is file_.IsValid(), a utility function from Chromium, returning -1 on atom.asar in atom/common/asar/archive.cc's Archive::Init().

By having a closer look at file.h, we see there is a method called error_details that returns Error that seems to be what we’re looking for:

// Returns the OS result of opening this file. Note that the way to verify
// the success of the operation is to use IsValid(), not this method:
//   File file(path, flags);
//   if (!file.IsValid())
//     return;
Error error_details() const { return error_details_; }


There is also a nice static method to convert Error to std::string:

// Converts an error value to a human-readable form. Used for logging.
static std::string ErrorToString(Error error);


By combining these two functions, the error becomes FILE_ERROR_ACCESS_DENIED.

Why access denied? We’re running the application as the superuser, who has access on everything, and we don’t get the error when running as a normal user.

See the following output from running the application with some improved error logging:

\$ sudo ./out/D/electron
33204
33204
33204
-1
Archive is invalid: (FILE_ERROR_ACCESS_DENIED): /home/jviotti/electron-current/out/D/resources/atom.asar
-1
...
Archive is invalid: (FILE_ERROR_ACCESS_DENIED): /home/jviotti/electron-current/out/D/resources/atom.asar
-1
Archive is invalid: (FILE_ERROR_ACCESS_DENIED): /home/jviotti/electron-current/out/D/resources/atom.asar
[30652:0128/113606:ERROR:CONSOLE(340)] "Uncaught Error: Cannot find module '/home/jviotti/electron-current/out/D/resources/atom.asar/renderer/lib/init.js'", source: module.js (340)


Notice that the operation actually succeds a couple of times before the access denied error. After some experimentation with getuid(), we can see the operation works from Electron’s main thread, but fails from renderer threads.

Turns out Chromium drops all capabilities with the capset Linux system call from renderer threads, which makes sense from a security point of view in the context of a web browser, but not much from the context of something like Electron:

// See https://code.google.com/p/chromium/codesearch#chromium/src/sandbox/linux/services/credentials.cc&q=ForkAndDrop&sq=package:chromium&type=cs&l=325

pid_t Credentials::ForkAndDropCapabilitiesInChild() {
pid_t pid = fork();
if (pid != 0) {
return pid;
}

// Since we just forked, we are single threaded.

Electron makes use of an awesome project called libchromiumcontent, which provides a shared library of Chromium and all its dependencies. The project contains a set of diff patches that are applied on Chromium’s source during the build phase, which is a perfect place for the fix.
I’ll write a blog post on how to submit a patch to libchromiumcontent and test your changes with Electron locally, stay tuned!