// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
// launcher_main.c implements a main method that launches
// `updater --server --service=update [--system] [logging flags]`.
// Because the launcher is sometimes used in a root setuid context, it has
// minimal dependencies and tries to harden the environment.
//
// If run with --test as the first argument, the launcher instead launches the
// updater with the `--test` flag instead of the `--server` flag. The updater
// will immediately exit in this case, but this is useful for testing the
// launcher.
//
// If run with --internal as the first argument, the launcher instead launches
// the updater with the `--service=update-internal` flag instead of the
// `--service=update` flag.
//
// In the system (setuid) context, the launcher verifies several security
// attributes of the binary it intends to launch, the path leading to the
// binary it intends to launch, and the non-chrootedness of its context; if any
// of these checks fail, it prints a diagnostic to stderr and returns a nonzero
// exit code (see <sysexits.h>).
//
// It isolates the subprocess from its calling environment by launching it into
// a new session, with a fixed environ and argv, with standard file handles
// pointing to /dev/null and all signal handling reset to default. macOS
// clears Mach exception ports before launching a setuid binary, so the
// launcher does not repeat this work. posix_spawn will fail (and the launcher
// will therefore print a diagnostic and return nonzero) if it cannot honor all
// of these settings.
//
// In the system context, the launcher resets its own bootstrap port to the
// privileged systemwide session, resets uid and gid to 0 (real, saved, and
// effective), resets rlimits to default values, resets its umask to 022,
// resets its working directory to '/', and resets its kernel security groups.
// It expects the subprocess to inherit all of these. It fails with a suitable
// diagnostic message and return value if any of these operations fail.
//
// This list of security checks and isolation mechanisms may not be exhaustive.
//
// If the launch is successful, it returns 0 (EX_OK). It does not become the
// subprocess and it does not wait for the subprocess to exit. It does not
// report the PID of the process it creates.
#include <CommonCrypto/CommonDigest.h>
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#include <bootstrap.h>
#include <err.h>
#include <errno.h>
#include <libproc.h>
#include <limits.h>
#include <mach/exception_types.h>
#include <mach/task.h>
#include <mach/task_special_ports.h>
#include <mach/vm_param.h>
#include <machine/vmparam.h>
#include <membership.h>
#include <pwd.h>
#include <signal.h>
#include <spawn.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/acl.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/syslimits.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/vnode.h>
#include <sysexits.h>
#include <unistd.h>
#include "chrome/updater/mac/launcher_constants.h"
#include "chrome/updater/updater_branding.h"
#define ARRAYSIZE(x) (sizeof(x) / sizeof(*(x)))
static bool StrAppend(char* dest, const char* suffix, size_t dest_size) {
return strlcat(dest, suffix, dest_size) < dest_size;
}
// ErrSec exits the program with the specified return code, emitting an error
// message of the form <msg>: <error code>.
static __attribute__((noreturn)) void ErrSec(int rc,
OSStatus error,
const char* msg) {
errx(rc, "%s: %d", msg, error);
}
// Checks whether extended POSIX ACLs allow write access on the specified path.
// ACLs for the specified principal are skipped -- this should be root's UUID.
static bool AclPermitsWrite(char const* const path, guid_t root_uuid) {
acl_t acl = acl_get_link_np(path, ACL_TYPE_EXTENDED);
if (!acl) {
if (errno == ENOENT) {
// No ACL is associated with this file.
return false;
}
err(EX_OSERR, "couldn't get acl on %s", path);
}
acl_entry_t entry = NULL;
for (int rc = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); !rc;
rc = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
acl_tag_t tag = 0;
if (acl_get_tag_type(entry, &tag)) {
err(EX_OSERR, "getting ACL tag for %s", path);
}
if (tag != ACL_EXTENDED_ALLOW) {
continue;
}
guid_t* principal_p = acl_get_qualifier(entry);
if (!principal_p) {
err(EX_OSERR, "getting ACL qualifier for %s", path);
}
if (!memcmp(principal_p->g_guid, root_uuid.g_guid, KAUTH_GUID_SIZE)) {
// Skip ACLs that grant permission to root.
acl_free(principal_p);
continue;
}
acl_free(principal_p);
uint64_t permset_mask = 0;
if (acl_get_permset_mask_np(entry, &permset_mask)) {
err(EX_OSERR, "getting ACL perms for %s", path);
}
if (permset_mask &
(ACL_WRITE_DATA | ACL_ADD_FILE | ACL_DELETE | ACL_APPEND_DATA |
ACL_ADD_SUBDIRECTORY | ACL_DELETE_CHILD | ACL_WRITE_ATTRIBUTES |
ACL_WRITE_EXTATTRIBUTES | ACL_WRITE_SECURITY | ACL_CHANGE_OWNER)) {
acl_free(acl);
return true;
}
}
acl_free(acl);
return false;
}
// VerifyPathOrDie verifies that path (must be absolute) and all of its prefixes
// are owned by root and cannot be written others. It iterates in root-to-leaf
// order to mitigate the opportunity for time-of-check/time-of-use flaws, since
// at each step, we know the path so far can only have been changed out from
// under us by something with root permissions. It forbids symlinks.
//
// If verification fails, the program prints an error (to stderr) and exits.
static void VerifyPathOrDie(const char* const path, guid_t root_uuid) {
if (path[0] != '/') {
errx(EX_SOFTWARE, "path not absolute: %s", path);
}
size_t len = strlen(path);
char* accumulating_path = calloc(strlen(path) + 1, 1);
for (size_t i = 0; i < len; ++i) {
accumulating_path[i] = path[i];
if (i && path[i + 1] != '/' && path[i + 1] != '\0') {
continue;
}
// We've reached root, some directory, or the full path; check it.
struct stat attribs = {};
if (lstat(accumulating_path, &attribs)) {
err(EX_NOPERM, "can't stat %s", accumulating_path);
}
if (S_ISLNK(attribs.st_mode)) {
errx(EX_OSFILE, "%s is a symlink", accumulating_path);
}
if (attribs.st_mode & 022) {
errx(EX_OSFILE, "loose permissions (0%o) on %s", attribs.st_mode & 07777,
accumulating_path);
}
if (attribs.st_uid) {
errx(EX_CONFIG, "non-root user %u owns %s", attribs.st_uid,
accumulating_path);
}
if (AclPermitsWrite(accumulating_path, root_uuid)) {
errx(EX_CONFIG, "loose permissions (extended ACL) on %s",
accumulating_path);
}
}
free(accumulating_path);
}
static bool IsChrooted() {
struct proc_vnodepathinfo vnodepathinfo = {};
if (proc_pidinfo(getpid(), PROC_PIDVNODEPATHINFO, 0, &vnodepathinfo,
sizeof(vnodepathinfo)) < 0) {
err(EX_OSERR, "proc_pidinfo");
}
return vnodepathinfo.pvi_rdir.vip_vi.vi_type == VDIR;
}
// An rlimit configuration for some specified resource. "which" is expected
// to be an RLIMIT constant.
typedef struct rlimit_config_struct {
int which;
struct rlimit rlimit;
} rlimit_config;
// Increase the limit for the resource defined by config->which to the values
// in config->soft (for rlim_cur) and config->hard (for rlim_max). This only
// ever increases limits; if the existing limit is already higher, it does
// not change the limit. If one part of the rlimit change is an increase and
// the other is not, it applies only the increase.
//
// If the getrlimit call to find existing values fails, this returns its
// return code (and does not change anything). Otherwise, it returns the
// return code from its call to setrlimit.
static int IncreaseRlimit(const rlimit_config* config) {
struct rlimit lim;
int rc = getrlimit(config->which, &lim);
if (rc) {
return rc;
}
if (lim.rlim_cur < config->rlimit.rlim_cur) {
lim.rlim_cur = config->rlimit.rlim_cur;
}
if (lim.rlim_max < config->rlimit.rlim_max) {
lim.rlim_max = config->rlimit.rlim_max;
}
// `cur` > `max` can occur for some `config`s, at least the maxproc one.
if (lim.rlim_cur > lim.rlim_max) {
lim.rlim_max = lim.rlim_cur;
}
return setrlimit(config->which, &lim);
}
// Array terminated with an rlimit_config with `which` set to
// kEndOfDefaultRlimits. Omits RLIMIT_NPROC, since its correct value is
// machine-specific; we use sysctlbyname to get the real caps and call
// setrlimit separately for this.
static const rlimit_config default_rlimits[] = {
{.which = RLIMIT_CORE,
.rlimit = {.rlim_cur = DFLCSIZ, .rlim_max = MAXCSIZ}},
{.which = RLIMIT_CPU,
.rlimit = {.rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY}},
{.which = RLIMIT_FSIZE,
.rlimit = {.rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY}},
{.which = RLIMIT_DATA,
.rlimit = {.rlim_cur = DFLDSIZ, .rlim_max = MAXDSIZ}},
{.which = RLIMIT_STACK,
.rlimit = {.rlim_cur = DFLSSIZ, .rlim_max = MAXSSIZ - PAGE_MAX_SIZE}},
{.which = RLIMIT_RSS,
.rlimit = {.rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY}},
{.which = RLIMIT_MEMLOCK,
.rlimit = {.rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY}},
{.which = RLIMIT_NOFILE,
.rlimit = {.rlim_cur = NOFILE, .rlim_max = OPEN_MAX}},
};
// Check whether the code object referenced via distant_code is validly signed.
// Returns an OSStatus in the errSec... space describing the result of the
// check.
//
// Like many Security.h APIs, the SecStaticCodeRef argument (distant_code) can
// be a "live" SecCodeRef instead. If a live SecCodeRef is provided, this check
// runs against the running image rather than the file on disk.
static OSStatus CheckSignature(SecStaticCodeRef distant_code) {
if (!distant_code) {
errx(EX_SOFTWARE, "can't check signature that doesn't exist");
}
SecRequirementRef req;
OSStatus rc = SecRequirementCreateWithString(
// Magic numbers from
// https://chromium.googlesource.com/chromium/src/+/7b1441f65d5bef3c0fe531809cccdeb40f9465c6/chrome/browser/safe_browsing/incident_reporting/binary_integrity_analyzer_mac.cc#49
CFSTR("anchor apple generic and "
"certificate 1[field.1.2.840.113635.100.6.2.6] and "
"certificate leaf[field.1.2.840.113635.100.6.1.13] and "
"certificate leaf[subject.OU] = "
"\"" MAC_TEAM_IDENTIFIER_STRING "\" and "
"identifier \"" MAC_BUNDLE_IDENTIFIER_STRING "\""),
kSecCSDefaultFlags, &req);
if (rc != errSecSuccess) {
ErrSec(EX_UNAVAILABLE, rc, "can't create requirement");
}
// We now have the pieces we need: a security requirement and the code
// signing object for the connecting process. Evaluate it, clean up our
// various Core Foundation objects, then return the verdict. Use live
// validity checks if possible.
if (CFGetTypeID(distant_code) == SecCodeGetTypeID()) {
SecCodeRef distant_live_code = (SecCodeRef)distant_code;
rc = SecCodeCheckValidity(distant_live_code, kSecCSDefaultFlags, req);
} else {
// This really is a static code ref.
rc = SecStaticCodeCheckValidity(distant_code,
kSecCSCheckAllArchitectures |
kSecCSCheckNestedCode |
kSecCSStrictValidate,
req);
}
CFRelease(req);
return rc;
}
static void Harden(const char* target_path) {
if (IsChrooted()) {
err(EX_CONFIG, "chrooted");
}
if (setuid(0)) {
err(EX_NOPERM, "can't setuid 0");
}
if (setgid(0)) {
err(EX_NOPERM, "can't setgid 0");
}
// Check supported platforms. POSIX_SPAWN_SETSID doesn't work until
// macOS 10.12, known internally to uname as 16.0.0.
struct utsname sysinfo = {};
if (uname(&sysinfo)) {
err(EX_OSERR, "can't get uname");
}
char* scan_end = NULL;
long os_major = strtol(sysinfo.release, &scan_end, 10);
if (scan_end == sysinfo.release) {
errx(EX_OSERR, "empty uname.release");
}
if (*scan_end != '.') {
errx(EX_PROTOCOL, "malformed uname.release (%s)", sysinfo.release);
}
if (os_major < 16L) {
errx(EX_UNAVAILABLE, "launcher requires macOS 10.12 or later");
}
// Set process count limits to system caps.
int32_t maxproc = -1;
size_t maxprocsz = sizeof(maxproc);
if (sysctlbyname("kern.maxproc", &maxproc, &maxprocsz, NULL, 0)) {
err(EX_UNAVAILABLE, "can't get kern.maxproc");
}
int32_t maxprocperuid = -1;
size_t maxprocperuidsz = sizeof(maxprocperuid);
if (sysctlbyname("kern.maxprocperuid", &maxprocperuid, &maxprocperuidsz, NULL,
0)) {
err(EX_UNAVAILABLE, "can't get kern.maxprocperuid");
}
rlimit_config nproc = {
.which = RLIMIT_NPROC,
.rlimit = {.rlim_cur = maxprocperuid, .rlim_max = maxproc}};
if (IncreaseRlimit(&nproc)) {
err(EX_OSERR, "can't set rlimit %d", RLIMIT_NPROC);
}
// Reset other resource limits.
for (size_t i = 0; i < ARRAYSIZE(default_rlimits); i++) {
if (IncreaseRlimit(&default_rlimits[i])) {
err(EX_OSERR, "can't set rlimit %d", default_rlimits[i].which);
}
}
// Find the startup port, the bootstrap port for all daemons. We use this as
// our bootstrap port so our requests to other root-level services can't be
// intercepted by something running in a user context. The subprocess will
// inherit this.
//
// The startup port is uniquely recognizable as the port that is its own
// parent.
//
// Empirical observation indicates that each next_port is likely to leak; each
// port acquires references not owned by this code (possibly from the MacOS
// APIs themselves). This implementation does not depend on this property,
// however.
mach_port_t startup_port = MACH_PORT_NULL;
mach_port_t next_port = bootstrap_port;
// Give next_port its own reference count for bootstrap_port so it won't be
// deallocated immediately if/when it "falls off" the end of this
// traversal.
kern_return_t kr = mach_port_mod_refs(mach_task_self(), bootstrap_port,
MACH_PORT_RIGHT_SEND, 1);
if (kr != KERN_SUCCESS && kr != KERN_INVALID_RIGHT) {
errx(EX_OSERR, "can't add send count for bootstrap_port: %d", kr);
}
do {
if (startup_port != MACH_PORT_NULL) {
mach_port_deallocate(mach_task_self(), startup_port);
}
startup_port = next_port;
kern_return_t bootstrap_err = bootstrap_parent(startup_port, &next_port);
if (bootstrap_err != KERN_SUCCESS) {
errx(EX_NOPERM, "bootstrap_parent: %d", bootstrap_err);
}
} while (startup_port != next_port);
// Release the the extra retains: transfer one right, one retain from `start`,
// and one retain from `next_port`.
task_set_bootstrap_port(mach_task_self(), startup_port);
mach_port_t old_bootstrap = bootstrap_port;
bootstrap_port = startup_port;
mach_port_deallocate(mach_task_self(), old_bootstrap);
mach_port_deallocate(mach_task_self(), next_port);
// initgroups and mbr_uid_to_uuid must be done only after bootstrap_port has
// become the startup port, since they make calls to opendirectoryd; looking
// it up can be redirected in a subset port, so using only the startup port
// ensures we're talking to the real one.
if (initgroups("root", 0)) {
err(EX_OSERR, "can't initgroups");
}
guid_t root_uuid;
if (mbr_uid_to_uuid(0, root_uuid.g_guid)) {
err(EX_OSERR, "can't get root's uuid");
}
// Verify paths. verifyPath is recursive and prints out error messages on its
// own if something goes wrong.
VerifyPathOrDie(target_path, root_uuid);
// Check signing.
if (kCheckSigning) {
CFStringRef cf_subprocess_path =
CFStringCreateWithCString(NULL, target_path, kCFStringEncodingUTF8);
CFURLRef subprocess_path_url = CFURLCreateWithFileSystemPath(
NULL, cf_subprocess_path, kCFURLPOSIXPathStyle, false);
CFRelease(cf_subprocess_path);
SecStaticCodeRef subprocess_code;
OSStatus os_rc = SecStaticCodeCreateWithPath(
subprocess_path_url, kSecCSDefaultFlags, &subprocess_code);
CFRelease(subprocess_path_url);
if (os_rc != errSecSuccess) {
ErrSec(EX_UNAVAILABLE, os_rc,
"can't get code signing info for subprocess");
}
os_rc = CheckSignature(subprocess_code);
if (os_rc != errSecSuccess) {
ErrSec(EX_CONFIG, os_rc, "subprocess verification failed");
}
CFRelease(subprocess_code);
CFStringRef cf_bundle_path =
CFStringCreateWithCString(NULL, kBundlePath, kCFStringEncodingUTF8);
CFURLRef bundle_path_url = CFURLCreateWithFileSystemPath(
NULL, cf_bundle_path, kCFURLPOSIXPathStyle, false);
SecStaticCodeRef bundle_code;
os_rc = SecStaticCodeCreateWithPath(bundle_path_url, kSecCSDefaultFlags,
&bundle_code);
CFRelease(bundle_path_url);
CFRelease(cf_bundle_path);
if (os_rc != errSecSuccess) {
ErrSec(EX_UNAVAILABLE, os_rc, "can't get code signing info for bundle");
}
os_rc = CheckSignature(bundle_code);
if (os_rc != errSecSuccess) {
ErrSec(EX_CONFIG, os_rc, "bundle verification failed");
}
CFRelease(bundle_code);
// Signing checks have passed.
}
}
static void Launch(
bool is_system, bool is_qualifying, bool is_internal, const char* path) {
if (chdir("/")) {
err(EX_OSFILE, "can't chdir to /");
}
umask(022);
alarm(0);
// Configure posix_spawn.
sigset_t empty_sigset; // Zero-initialization of sigset_t is not portable.
sigemptyset(&empty_sigset);
sigset_t full_sigset;
sigfillset(&full_sigset);
posix_spawnattr_t spawn_attrs = NULL;
int posix_err = posix_spawnattr_init(&spawn_attrs);
if (posix_err) {
errc(EX_UNAVAILABLE, posix_err, "can't init spawn attrs");
}
posix_err = posix_spawnattr_setflags(
&spawn_attrs, POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK |
POSIX_SPAWN_SETSID | POSIX_SPAWN_CLOEXEC_DEFAULT);
if (posix_err) {
errc(EX_OSERR, posix_err, "can't set spawn flags");
}
posix_err = posix_spawnattr_setsigdefault(&spawn_attrs, &full_sigset);
if (posix_err) {
errc(EX_OSERR, posix_err, "can't set default signal handlers");
}
posix_err = posix_spawnattr_setsigmask(&spawn_attrs, &empty_sigset);
if (posix_err) {
errc(EX_OSERR, posix_err, "can't clear signal mask");
}
cpu_type_t cpu_any = CPU_TYPE_ANY;
size_t ocount = 0;
posix_err = posix_spawnattr_setbinpref_np(&spawn_attrs, 1, &cpu_any, &ocount);
if (posix_err) {
errc(EX_OSERR, posix_err, "can't set CPU_TYPE_ANY binpref");
}
posix_err = posix_spawnattr_setspecialport_np(&spawn_attrs, bootstrap_port,
TASK_BOOTSTRAP_PORT);
if (posix_err) {
errc(EX_OSERR, posix_err, "can't set bootstrap port");
}
// There is no need to clear task-level exception ports, because in the isRoot
// context, macOS already did that as a consequence of launching a setuid
// process. Refer to kern_exec.c in xnu source.
posix_spawn_file_actions_t file_actions = NULL;
posix_err = posix_spawn_file_actions_init(&file_actions);
if (posix_err) {
errc(EX_UNAVAILABLE, posix_err, "can't init file actions");
}
posix_err = posix_spawn_file_actions_addopen(&file_actions, STDIN_FILENO,
"/dev/null", O_RDONLY, 0);
if (posix_err) {
errc(EX_OSERR, posix_err, "can't point /dev/null to stdin");
}
posix_err = posix_spawn_file_actions_addopen(&file_actions, STDOUT_FILENO,
"/dev/null", O_WRONLY, 0);
if (posix_err) {
errc(EX_OSERR, posix_err, "can't point stdout to /dev/null");
}
posix_err = posix_spawn_file_actions_adddup2(&file_actions, STDOUT_FILENO,
STDERR_FILENO);
if (posix_err) {
errc(EX_OSERR, posix_err,
"can't point stderr to /dev/null (as dup2 of stdout)");
}
char* const argv[] = {
(char*)kExecutableName, // posix_spawn will not overwrite the argv.
is_qualifying ? "--test" : "--server",
is_internal ? "--service=update-internal" : "--service=update",
is_system ? "--system" : NULL,
NULL};
static char* const env[] = {"PWD=/", "PATH=/usr/bin:/bin:/usr/sbin:/sbin",
NULL};
posix_err = posix_spawn(NULL, path, &file_actions, &spawn_attrs, argv, env);
if (posix_err) {
errc(EX_UNAVAILABLE, posix_err, "posix_spawn failed");
}
}
void UserMain(uid_t euid, bool is_qualifying, bool is_internal) {
// Find home directory.
const char* home = getenv("HOME");
if (!home) {
// getpwuid is thread-safe on macOS. The program may become multi-threaded
// once we invoke Apple APIs.
struct passwd* pwd = getpwuid(euid);
if (pwd) {
home = pwd->pw_dir;
}
}
if (!home) {
err(EX_OSERR, "unable to find user homedir");
}
char path[PATH_MAX] = "";
if (!StrAppend(path, home, ARRAYSIZE(path))) {
err(EX_OSERR, "path to homedir is too long");
}
if (!StrAppend(path, kExecutablePath, ARRAYSIZE(path))) {
err(EX_OSERR, "path to updater executable is too long");
}
Launch(false, is_qualifying, is_internal, path);
}
void SystemMain(bool is_qualifying, bool is_internal) {
Harden(kExecutablePath);
Launch(true, is_qualifying, is_internal, kExecutablePath);
}
int main(int argc, char** argv) {
const uid_t euid = geteuid();
bool is_qualifying = argc >= 2 && strcmp("--test", argv[1]) == 0;
bool is_internal = argc >= 2 && strcmp("--internal", argv[1]) == 0;
if (euid == 0) {
SystemMain(is_qualifying, is_internal);
} else {
UserMain(euid, is_qualifying, is_internal);
}
return EX_OK;
}