Running Firefox in a cgroup (using systemd)

This blog post is very out of date. As of 2020, you can find up to date information about this topic in the LWN article “Resource management for the desktop”

I’m a long time user of Firefox and it’s a pretty good browser but you know how sometimes it eats all of the memory on your computer and uses lots of CPU so the whole thing becomes completely unusable? That is incredibly annoying!

I’ve been using a build system with a web interface recently and it is really a problem there, because build logs can be quite large (40MB) and Firefox handles them really badly.

Linux has been theoretically able to limit how much CPU, RAM and IO that a process can use for some time, with the cgroups mechanism. Its default behavour, at the time of writing, is to let Firefox starve out all other processes so that I am totally unable to kill it and have to force power-off on my computer and restart it. It would make much more sense for Linux’s scheduler to ensure that the user interface always gets some CPU time, so I can kill programs that are going nuts, and also for the out-of-memory killer to actually work properly. There is a proposal to integrate gnome-session with systemd which I hope would solve this problem for me. But in the meantime, here’s a fairly hacky way of making sure that Firefox always runs in a cgroup with a fixed amount of memory, so that it will crash itself when it tries to use too much RAM instead of making your computer completely unusable.

I’m using Fedora 20 right now, but probably any operating system with Linux and systemd will work the same.

First, you need to create a ‘slice’. The documentation for this stuff is quite dense but the concept is simple: your system’s resources get divided up into slices. Slices are heirarchical, and there are some predefined slices that systemd provides including user.slice (for user applications) and system.slice (for system services). So I made a user-firefox.slice:

[Unit]
Description=Firefox Slice
Before=slices.target

[Slice]
MemoryAccounting=true
MemoryLimit=512M
# CPUQuota isn't available in systemd 208 (Fedora 20).
#CPUAccounting=true
#CPUQuota=25%

This should be saved as /etc/systemd/system/user-firefox.slice. Then you can run systemctl daemon-reload && systemctl restart user-firefox.slice and your slice is created with its resource limit!

You can now run a command in this slice using the systemd-run command, as root.

sudo systemd-run --slice user-firefox.slice --scope xterm

The xterm process and anything you run from it will be limited to using 512MB of RAM, and memory allocations will fail for them if more than that is used. Most programs crash when this happens because nobody really checks the result of malloc() (or they do check it, but they never tested the code path that runs if an allocation fails so it probably crashes anyway). If you want to be confident this is working, change the MemoryLimit in user-firefox.slice to 10M and run a desktop application: probably it will crash before it even starts (you need to daemon-reload and restart the .slice after you edit the file for the changes to take effect).

About the --scope argument: a ‘scope’ is basically a way of identifying one or more processes that aren’t being managed directly by systemd. By default, systemd-run would start xterm as a system service, which wouldn’t work because it would be isolated from the X server.

So now you can run Firefox in a cgroup, but it’s a bit shit because you can only do so as the ‘root’ user. You’ll find if you try to use `sudo` or `su` to become your user again that these create a new systemd user session that is outside the user-firefox.slice cgroup. You can use `systemd-cgls` to show the heirarchy of slices, and you’ll see that the commands run under `sudo` or `su` show up in a new scope called something like session-c3.scope, where the scope created by systemd-run that is in the correct slice is called run-1234.scope.

There are various nice ways that we could go about fixing this but today I am not going to be a nice person, instead I just wrote this Python wrapper that becomes my user and then runs Firefox from inside the same scope:

#!/usr/bin/env python3

import os
import pwd


user_info = pwd.getpwnam('sam')
os.setuid(user_info.pw_uid)

env = os.environ.copy()
env['HOME'] = user_info.pw_dir

os.execle('/usr/bin/firefox', 'Firefox (tame)', env)

Now I can run:

sudo systemd-run --slice user-firefox.slice --scope
./user-firefox

This is a massive hack and don’t hold me responsible for anything bad that may come of it. Please contribute to Firefox if you can.

4 thoughts on “Running Firefox in a cgroup (using systemd)

  1. A quick, ad-hoc way to do this is:

    “systemd-run –user –slice=user-firefox –unit=user-firefox -p MemoryHigh=512M -p CPUQuota=25% firefox”

    It will create a transient slice and service unit using the systemd user session for that login. You don’t need a scope unit or root privs.

    On fedora, you may have to enable a SELinux boolean to let your user manage cgroups. The name of the property for memory limits may change depending on which version of cgroups you’re using (v1 or v2). Details are in systemd.resource-control.

    Posting this here since a web search for “systemd firefox limit memory” keeps on bringing me to this post.

    1. Thanks! In the 4 years since this post, Firefox has got a lot better with memory usage, but it’s still useful to know. And thankfully systemd user sessions are finally being rolled out in some distros!

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.