diff --git a/mozregression/cli.py b/mozregression/cli.py index efc1b4bfb..c45681d75 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="Dont check for unprivileged user namespaces AppArmor blocking.", + ) + return parser diff --git a/mozregression/main.py b/mozregression/main.py index b0d50a4dc..1dfb4273e 100644 --- a/mozregression/main.py +++ b/mozregression/main.py @@ -308,6 +308,81 @@ def check_mozregression_version(): ) +def check_unprivileged_userns(): + """ + 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). + """ + + apparmor_file = "/proc/sys/kernel/apparmor_restrict_unprivileged_userns" + if not os.path.isfile(apparmor_file): + return + + with open(apparmor_file, "r") as f: + if f.read().strip() != "1": + return + + import ctypes + import errno + import signal + + # TODO: read from system? + # + SYS_clone = 56 + + libc = ctypes.CDLL(None, use_errno=True) + + LOG.warning( + "Unprivileged user namespaces might be disabled. Checking clone() + unshare() syscalls ..." + ) + + pid = libc.syscall(SYS_clone, signal.SIGCHLD.value | os.CLONE_NEWUSER, None, None, None, None) + print("pid", pid) + + if pid == 0: + # Child side ... + rv = libc.unshare(os.CLONE_NEWPID) + _errno = ctypes.get_errno() + print("rv=", rv) + if rv < 0: + print("rv", rv, "errno", _errno) + sys.exit(_errno) + sys.exit(0) + else: + print("waiting", pid) + (pid, statuscode) = os.waitpid(pid, 0) + exitcode = os.waitstatus_to_exitcode(statuscode) + print("waited", pid, "finshed with", statuscode, " => ", exitcode) + + if exitcode == 0: + return + + if exitcode == errno.EPERM: + LOG.warning( + "Unprivileged user namespaces are disabled. This is likely because AppArmor policy " + "change. Please refer to XXX to learn how to setup AppArmor so that MozRegression " + "works correctly. Missing AppArmor profile can lead to crashes or to incorrectly " + "sandboxed processes." + ) + return + + LOG.warning( + "Unexpected exit code {} while performing user namespace " + "checks. You might want to file a bug.".format(exitcode) + ) + + def main( argv=None, namespace=None, @@ -327,6 +402,15 @@ def main( if check_new_version: check_mozregression_version() config.validate() + if ( + sys.platform + in ( + "linux", + "linux2", + ) + and not config.options.dont_check_userns + ): + check_unprivileged_userns() set_http_session(get_defaults={"timeout": config.options.http_timeout}) app = Application(config.fetch_config, config.options)