From cbffd3c86d9d6b4bb77e79b2577d71d9be0a2347 Mon Sep 17 00:00:00 2001 From: Alexandre Lissy Date: Wed, 29 May 2024 14:46:34 +0200 Subject: [PATCH] Bug 1899515 - Inform users of maybe missing AppArmor rules --- docs/documentation/usage.md | 38 +++++++++++ mozregression/cli.py | 9 +++ mozregression/config.py | 1 + mozregression/linux_utils.py | 122 +++++++++++++++++++++++++++++++++++ mozregression/main.py | 10 +++ 5 files changed, 180 insertions(+) create mode 100644 mozregression/linux_utils.py diff --git a/docs/documentation/usage.md b/docs/documentation/usage.md index 658b07e19..50e52fd4a 100644 --- a/docs/documentation/usage.md +++ b/docs/documentation/usage.md @@ -148,3 +148,41 @@ to date list of available options. - List firefox releases numbers mozregression --list-releases + +## Unprivileged user namespaces + +AppArmor can be used to restrict the usage of this kernel feature, which is +known to have shipped with at least Ubuntu 24.04 [AppArmor Ubuntu 24.04](https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/2046844). + +This restricts the ability of the Firefox sandbox and might impair a +mozregression bisection because of the difference of behavior. Also, builds +before [Bug 1884347](https://bugzilla.mozilla.org/show_bug.cgi?id=1884347) will +just crash their content processes. + +You can either disable the AppArmor restriction or install an AppArmor profile, +e.g. at `/etc/apparmor.d/firefox-mozregression` with the following content that +will grant the `userns` permission to Firefox binaries under +`/tmp/mozregression/*`: + + abi , + include + + profile firefox-mozregression /tmp/mozregression/*/firefox/firefox{,-bin} flags=(unconfined) { + userns, + + # Site-specific additions and overrides. See local/README for details. + include if exists + } + +Then apply with a + + sudo systemctl restart apparmor.service + +You can then prefix your `mozregression` calls with a `TMP=` to make use of the +aforthmentionned directory where `userns` is granted: + + TMP=/tmp/mozregression/ mozregression [...] + +There was a window of time during which Firefox incorrectly tested for that +feature and it would end up in tab crashing. In case of doubt you can always +set environment variable `MOZ_ASSUME_USER_NS=0` before running `mozregression`. diff --git a/mozregression/cli.py b/mozregression/cli.py index 835ec623d..83d3a9591 100644 --- a/mozregression/cli.py +++ b/mozregression/cli.py @@ -419,6 +419,12 @@ def create_parser(defaults): parser.add_argument("--debug", "-d", action="store_true", help="Show the debug output.") + parser.add_argument( + "--dont-check-userns", + action="store_true", + help="Do not check for unprivileged user namespaces AppArmor blocking.", + ) + return parser @@ -515,6 +521,9 @@ def __init__(self, options, config): ) self.enable_telemetry = config["enable-telemetry"] not in ("no", "false", 0) + self.dont_check_userns = ( + config["dont-check-userns"] in ("yes", "true", 1) or self.options.dont_check_userns + ) self.action = None self.fetch_config = None diff --git a/mozregression/config.py b/mozregression/config.py index eef15df91..2bcd3715b 100644 --- a/mozregression/config.py +++ b/mozregression/config.py @@ -48,6 +48,7 @@ "taskcluster-accesstoken": None, "taskcluster-clientid": None, "enable-telemetry": True, + "dont-check-userns": False, } diff --git a/mozregression/linux_utils.py b/mozregression/linux_utils.py new file mode 100644 index 000000000..5c294b38b --- /dev/null +++ b/mozregression/linux_utils.py @@ -0,0 +1,122 @@ +""" +Various linux-specific tools +""" + +import os +import sys + + +def check_unprivileged_userns(logger): + """ + Some distribution started to block unprivileged user namespaces via + AppArmor. This might result in crashes on older builds, and in degraded + sandbox behavior. It is fixed with an AppArmor profile that allows the + syscall to proceed, but this is path dependant on the binary we download + and needs to be installed at a system level, so we can only advise people + of the situation. + + The following sys entry should be enough to verify whether it is blocked or + not, but the Ubuntu security team recommend cross-checking with actual + syscall. This code is a simplification of how Firerox does it, cf + https://searchfox.org/mozilla-central/rev/23efe2c8c5b3a3182d449211ff9036fb34fe0219/security/sandbox/linux/SandboxInfo.cpp#114-175 + and has been the most reliable way so far (shell with unshare would not + reproduce EPERM like we want). + + Return False if there is no problem or True if the user needs to fix their + setup. + """ + + apparmor_file = "/proc/sys/kernel/apparmor_restrict_unprivileged_userns" + if not os.path.isfile(apparmor_file): + return False + + with open(apparmor_file, "r") as f: + if f.read().strip() != "1": + return False + + import ctypes + import errno + import platform + import signal + + # Values are from + # https://github.com/hrw/syscalls-table/tree/163e238e4d7761fcf6ac500aad92d53ac88d663a/system_calls/tables + # imported from linux kernel headers + SYS_clone = { + "i386": 120, + "x32": 1073741880, + "x86_64": 56, + "arm": 120, + "armv7l": 120, + "arm64": 220, + "aarch64": 220, + "aarch64_be": 220, + "armv8b": 220, + "armv8l": 220, + }.get(platform.machine()) + if not SYS_clone: + logger.warning( + "Unprivileged user namespaces might be disabled, but unsupported platform? {}".format( + platform.machine() + ) + ) + return False + + libc = ctypes.CDLL(None, use_errno=True) + + logger.warning( + "Unprivileged user namespaces might be disabled. Checking clone() + unshare() syscalls ..." + ) + + try: + # Introduced in 3.12 which is the version of Ubuntu 24.04 + clone_newuser = os.CLONE_NEWUSER + clone_newpid = os.CLONE_NEWPID + except AttributeError: + # From + # https://github.com/torvalds/linux/blob/5bbd9b249880dba032bffa002dd9cd12cd5af09c/include/uapi/linux/sched.h#L31-L32 + # Last change 12 years ago, so it should be a stable fallback + clone_newuser = 0x10000000 + clone_newpid = 0x20000000 + + pid = libc.syscall(SYS_clone, signal.SIGCHLD.value | clone_newuser, None, None, None, None) + + if pid == 0: + # Child side ... + rv = libc.unshare(clone_newpid) + _errno = ctypes.get_errno() + if rv < 0: + sys.exit(_errno) + sys.exit(0) + else: + (pid, statuscode) = os.waitpid(pid, 0) + exitcode = os.waitstatus_to_exitcode(statuscode) + + if exitcode == 0: + return False + + if exitcode == errno.EPERM: + logger.warning( + "Unprivileged user namespaces is disabled. This is likely because AppArmor policy " + "change. Please refer to {} to learn how to setup AppArmor so that mozregression " + "works correctly. Missing AppArmor profile can lead to crashes or to incorrectly " + "sandboxed processes.".format( + "https://mozilla.github.io/mozregression/documentation/usage.html#unprivileged-user-namespaces" # noqa: E501 + ) + ) + logger.warning( + "If you already applied the suggested fix, then this warning can be ignored. " + "It can also be silenced by the --dont-check-userns flag." + "Another side effect is that browser's tab may crash because " + "they incorrectly test for the feature. If your regression " + "window covers that, you may want to set MOZ_ASSUME_USER_NS=0 " + "environmnent variable before launching mozregression." + ) + return True + + logger.warning( + "Unexpected exit code {} while performing user namespace " + "checks. You might want to file a bug.".format(exitcode) + ) + + return False diff --git a/mozregression/main.py b/mozregression/main.py index b0d50a4dc..a486e2517 100644 --- a/mozregression/main.py +++ b/mozregression/main.py @@ -26,6 +26,7 @@ from mozregression.fetch_build_info import IntegrationInfoFetcher, NightlyInfoFetcher from mozregression.json_pushes import JsonPushes from mozregression.launchers import REGISTRY as APP_REGISTRY +from mozregression.linux_utils import check_unprivileged_userns from mozregression.network import set_http_session from mozregression.persist_limit import PersistLimit from mozregression.telemetry import UsageMetrics, get_system_info, send_telemetry_ping_oop @@ -327,6 +328,15 @@ def main( if check_new_version: check_mozregression_version() config.validate() + if ( + sys.platform + in ( + "linux", + "linux2", + ) + and not config.dont_check_userns + ): + check_unprivileged_userns(LOG) set_http_session(get_defaults={"timeout": config.options.http_timeout}) app = Application(config.fetch_config, config.options)