Bug 1858279 - Rework how process launch interacts with Linux sandboxing and the fork server. r=nika,gcp

Currently, process launch interfaces with Linux sandboxing via the
ForkDelegate abstraction, basically replacing `fork` with an opaque
stateful callback, configured using various info from the parent process
(prefs, gfxInfo, etc.).  Unfortunately, the fork server effectively
needs to move that object into another process, and this is accomplished
in a way that's complicated and difficult to deal with and causes some
problems.

Instead, this patch makes the sandboxing state transparent: fields
are added to LaunchOptions which are serialized/deserialized, and the
sandbox launcher object is now exposed in a header and used directly by
LaunchApp (and its fork server equivalent).

There are a few other changes that follow from this.  In particular,
the pipe for the chroot server is now created later, during LaunchApp
but before `FileDescriptorShuffle::Init`, so LaunchApp will side-effect
`LaunchOptions::fds_to_remap`.  (But this also means we're no longer
using a fake mapping of fd 10 which isn't actually used, and we're no
longer creating a socketpair in one process and sending both ends to
another process that could have just created it itself.)

For more details, see the comments in `SandboxLaunch.h` for the member
functions `Configure`, `Prepare`, and `Fork`.

As a convenient side effect of this change, `Prepare` is now fallible,
so we can handle certain error cases (like failing to create a socket
pair) more gracefully.

Differential Revision: https://phabricator.services.mozilla.com/D194456
This commit is contained in:
Jed Davis 2023-12-02 01:49:58 +00:00
Родитель 4e62dae735
Коммит 6c9a2c06a5
9 изменённых файлов: 194 добавлений и 158 удалений

Просмотреть файл

@ -163,15 +163,16 @@ struct LaunchOptions {
bool use_forkserver = false;
#endif
#if defined(XP_LINUX)
struct ForkDelegate {
virtual ~ForkDelegate() {}
virtual pid_t Fork() = 0;
};
// If non-null, the fork delegate will be called instead of fork().
// It is not required to call pthread_atfork hooks.
mozilla::UniquePtr<ForkDelegate> fork_delegate = nullptr;
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
// These fields are used by the sandboxing code in SandboxLaunch.cpp.
// It's not ideal to have them here, but trying to abstract them makes
// it harder to serialize LaunchOptions for the fork server.
//
// (fork_flags holds extra flags for the clone() syscall, and
// sandbox_chroot indicates whether the child process will be given
// the ability to chroot() itself to an empty directory.)
int fork_flags = 0;
bool sandbox_chroot = false;
#endif
#ifdef XP_DARWIN

Просмотреть файл

@ -16,6 +16,10 @@
#include "mozilla/ipc/LaunchError.h"
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
# include "mozilla/SandboxLaunch.h"
#endif
#if defined(MOZ_ENABLE_FORKSERVER)
# include <stdlib.h>
# include <fcntl.h>
@ -93,6 +97,17 @@ bool AppProcessBuilder::ForkProcess(const std::vector<std::string>& argv,
}
});
# if defined(XP_LINUX) && defined(MOZ_SANDBOX)
mozilla::SandboxLaunch launcher;
if (!launcher.Prepare(&options)) {
return false;
}
# else
struct {
pid_t Fork() { return fork(); }
} launcher;
# endif
argv_ = argv;
if (!shuffle_.Init(options.fds_to_remap)) {
return false;
@ -103,16 +118,12 @@ bool AppProcessBuilder::ForkProcess(const std::vector<std::string>& argv,
fflush(stdout);
fflush(stderr);
# ifdef XP_LINUX
pid_t pid = options.fork_delegate ? options.fork_delegate->Fork() : fork();
pid_t pid = launcher.Fork();
// WARNING: if pid == 0, only async signal safe operations are permitted from
// here until exec or _exit.
//
// Specifically, heap allocation is not safe: the sandbox's fork substitute
// won't run the pthread_atfork handlers that fix up the malloc locks.
# else
pid_t pid = fork();
# endif
if (pid < 0) {
return false;
@ -209,23 +220,31 @@ static Result<Ok, LaunchError> LaunchAppWithForkServer(
ProcessHandle* process_handle) {
MOZ_ASSERT(ForkServiceChild::Get());
nsTArray<nsCString> _argv(argv.size());
nsTArray<mozilla::EnvVar> env(options.env_map.size());
nsTArray<mozilla::FdMapping> fdsremap(options.fds_to_remap.size());
// Check for unsupported options
MOZ_ASSERT(options.workdir.empty());
MOZ_ASSERT(!options.full_env);
MOZ_ASSERT(!options.wait);
ForkServiceChild::Args forkArgs;
# if defined(XP_LINUX) && defined(MOZ_SANDBOX)
forkArgs.mForkFlags = options.fork_flags;
forkArgs.mChroot = options.sandbox_chroot;
# endif
for (auto& arg : argv) {
_argv.AppendElement(arg.c_str());
forkArgs.mArgv.AppendElement(arg.c_str());
}
for (auto& vv : options.env_map) {
env.AppendElement(mozilla::EnvVar(nsCString(vv.first.c_str()),
forkArgs.mEnv.AppendElement(mozilla::EnvVar(nsCString(vv.first.c_str()),
nsCString(vv.second.c_str())));
}
for (auto& fdmapping : options.fds_to_remap) {
fdsremap.AppendElement(mozilla::FdMapping(
forkArgs.mFdsRemap.AppendElement(mozilla::FdMapping(
mozilla::ipc::FileDescriptor(fdmapping.first), fdmapping.second));
}
return ForkServiceChild::Get()->SendForkNewSubprocess(_argv, env, fdsremap,
return ForkServiceChild::Get()->SendForkNewSubprocess(forkArgs,
process_handle);
}
#endif // MOZ_ENABLE_FORKSERVER
@ -241,6 +260,17 @@ Result<Ok, LaunchError> LaunchApp(const std::vector<std::string>& argv,
mozilla::UniquePtr<char*[]> argv_cstr(new char*[argv.size() + 1]);
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
mozilla::SandboxLaunch launcher;
if (!launcher.Prepare(&options)) {
return Err(LaunchError("SL::Prepare", errno));
}
#else
struct {
pid_t Fork() { return fork(); }
} launcher;
#endif
EnvironmentArray env_storage;
const EnvironmentArray& envp =
options.full_env ? options.full_env
@ -267,16 +297,12 @@ Result<Ok, LaunchError> LaunchApp(const std::vector<std::string>& argv,
const char* gcov_child_prefix = PR_GetEnv("GCOV_CHILD_PREFIX");
#endif
#ifdef XP_LINUX
pid_t pid = options.fork_delegate ? options.fork_delegate->Fork() : fork();
pid_t pid = launcher.Fork();
// WARNING: if pid == 0, only async signal safe operations are permitted from
// here until exec or _exit.
//
// Specifically, heap allocation is not safe: the sandbox's fork substitute
// won't run the pthread_atfork handlers that fix up the malloc locks.
#else
pid_t pid = fork();
#endif
if (pid < 0) {
CHROMIUM_LOG(WARNING) << "fork() failed: " << strerror(errno);

Просмотреть файл

@ -155,6 +155,12 @@ inline bool ParseForkNewSubprocess(IPC::Message& aMsg,
nsTArray<EnvVar> env_map;
nsTArray<FdMapping> fds_remap;
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
ReadParamInfallible(&reader, &aOptions->fork_flags,
"Error deserializing 'int'");
ReadParamInfallible(&reader, &aOptions->sandbox_chroot,
"Error deserializing 'bool'");
#endif
ReadParamInfallible(&reader, &argv_array,
"Error deserializing 'nsCString[]'");
ReadParamInfallible(&reader, &env_map, "Error deserializing 'EnvVar[]'");
@ -203,10 +209,6 @@ void ForkServer::OnMessageReceived(UniquePtr<IPC::Message> message) {
return;
}
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
mozilla::SandboxLaunchForkServerPrepare(argv, options);
#endif
base::ProcessHandle child_pid = -1;
mAppProcBuilder = MakeUnique<base::AppProcessBuilder>();
if (!mAppProcBuilder->ForkProcess(argv, std::move(options), &child_pid)) {

Просмотреть файл

@ -72,15 +72,18 @@ ForkServiceChild::~ForkServiceChild() {
}
Result<Ok, LaunchError> ForkServiceChild::SendForkNewSubprocess(
const nsTArray<nsCString>& aArgv, const nsTArray<EnvVar>& aEnvMap,
const nsTArray<FdMapping>& aFdsRemap, pid_t* aPid) {
const Args& aArgs, pid_t* aPid) {
mRecvPid = -1;
IPC::Message msg(MSG_ROUTING_CONTROL, Msg_ForkNewSubprocess__ID);
IPC::MessageWriter writer(msg);
WriteIPDLParam(&writer, nullptr, aArgv);
WriteIPDLParam(&writer, nullptr, aEnvMap);
WriteIPDLParam(&writer, nullptr, aFdsRemap);
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
WriteIPDLParam(&writer, nullptr, aArgs.mForkFlags);
WriteIPDLParam(&writer, nullptr, aArgs.mChroot);
#endif
WriteIPDLParam(&writer, nullptr, aArgs.mArgv);
WriteIPDLParam(&writer, nullptr, aArgs.mEnv);
WriteIPDLParam(&writer, nullptr, aArgs.mFdsRemap);
if (!mTcver->Send(msg)) {
MOZ_LOG(gForkServiceLog, LogLevel::Verbose,
("the pipe to the fork server is closed or having errors"));

Просмотреть файл

@ -33,6 +33,16 @@ class ForkServiceChild {
ForkServiceChild(int aFd, GeckoChildProcessHost* aProcess);
virtual ~ForkServiceChild();
struct Args {
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
int mForkFlags = 0;
bool mChroot = false;
#endif
nsTArray<nsCString> mArgv;
nsTArray<EnvVar> mEnv;
nsTArray<FdMapping> mFdsRemap;
};
/**
* Ask the fork server to create a new process with given parameters.
*
@ -45,9 +55,7 @@ class ForkServiceChild {
* \param aPid returns the PID of the content process created.
* \return true if success.
*/
Result<Ok, LaunchError> SendForkNewSubprocess(
const nsTArray<nsCString>& aArgv, const nsTArray<EnvVar>& aEnvMap,
const nsTArray<FdMapping>& aFdsRemap, pid_t* aPid);
Result<Ok, LaunchError> SendForkNewSubprocess(const Args& aArgs, pid_t* aPid);
/**
* Create a fork server process and the singleton of this class.

Просмотреть файл

@ -614,7 +614,7 @@ void GeckoChildProcessHost::PrepareLaunch() {
}
#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
SandboxLaunchPrepare(mProcessType, mLaunchOptions.get(), mSandbox);
SandboxLaunch::Configure(mProcessType, mSandbox, mLaunchOptions.get());
#endif
#ifdef XP_WIN

Просмотреть файл

@ -12,9 +12,6 @@
namespace mozilla {
static const int kSandboxChrootClientFd = 6;
#if defined(MOZ_ENABLE_FORKSERVER)
static const int kSandboxChrootServerFd = 10;
#endif
static const char kSandboxChrootRequest = 'C';
static const char kSandboxChrootResponse = 'O';
static const char kSandboxChrootEnvFlag[] = "MOZ_SANDBOX_USE_CHROOT";

Просмотреть файл

@ -220,47 +220,30 @@ static void AttachSandboxReporter(base::file_handle_mapping_vector* aFdMap) {
aFdMap->push_back({srcFd, dstFd});
}
class SandboxFork : public base::LaunchOptions::ForkDelegate {
public:
explicit SandboxFork(int aFlags, bool aChroot, int aServerFd = -1,
int aClientFd = -1);
virtual ~SandboxFork();
void PrepareMapping(base::file_handle_mapping_vector* aMap);
pid_t Fork() override;
private:
int mFlags;
int mChrootServer;
int mChrootClient;
void StartChrootServer();
SandboxFork(const SandboxFork&) = delete;
SandboxFork& operator=(const SandboxFork&) = delete;
};
static int GetEffectiveSandboxLevel(GeckoProcessType aType,
ipc::SandboxingKind aKind) {
auto info = SandboxInfo::Get();
switch (aType) {
case GeckoProcessType_GMPlugin:
if (info.Test(SandboxInfo::kEnabledForMedia)) {
return 1;
}
return 0;
case GeckoProcessType_Content:
#ifdef MOZ_ENABLE_FORKSERVER
// With this env MOZ_SANDBOXED will be set, and mozsandbox will
// be preloaded for the fork server. The content processes rely
// on wrappers defined by mozsandbox to work properly.
// be preloaded for the fork server. Sandboxed child processes
// rely on wrappers defined by mozsandbox to work properly.
case GeckoProcessType_ForkServer:
return 1;
break;
#endif
case GeckoProcessType_Content:
// GetEffectiveContentSandboxLevel is main-thread-only due to prefs.
MOZ_ASSERT(NS_IsMainThread());
if (info.Test(SandboxInfo::kEnabledForContent)) {
return GetEffectiveContentSandboxLevel();
}
return 0;
case GeckoProcessType_GMPlugin:
if (info.Test(SandboxInfo::kEnabledForMedia)) {
return 1;
}
return 0;
case GeckoProcessType_RDD:
return PR_GetEnv("MOZ_DISABLE_RDD_SANDBOX") == nullptr ? 1 : 0;
case GeckoProcessType_Socket:
@ -274,8 +257,10 @@ static int GetEffectiveSandboxLevel(GeckoProcessType aType,
}
}
void SandboxLaunchPrepare(GeckoProcessType aType, base::LaunchOptions* aOptions,
ipc::SandboxingKind aKind) {
// static
void SandboxLaunch::Configure(GeckoProcessType aType, SandboxingKind aKind,
LaunchOptions* aOptions) {
MOZ_ASSERT(aOptions->fork_flags == 0 && !aOptions->sandbox_chroot);
auto info = SandboxInfo::Get();
// We won't try any kind of sandboxing without seccomp-bpf.
@ -364,82 +349,18 @@ void SandboxLaunchPrepare(GeckoProcessType aType, base::LaunchOptions* aOptions,
if (canChroot || flags != 0) {
flags |= CLONE_NEWUSER;
auto forker = MakeUnique<SandboxFork>(flags, canChroot);
forker->PrepareMapping(&aOptions->fds_to_remap);
aOptions->fork_delegate = std::move(forker);
// Pass to |SandboxLaunchForkServerPrepare()| in the fork server.
aOptions->env_map[kSandboxChrootEnvFlag] =
std::to_string(canChroot ? 1 : 0) + std::to_string(flags);
}
aOptions->env_map[kSandboxChrootEnvFlag] = std::to_string(canChroot ? 1 : 0);
aOptions->sandbox_chroot = canChroot;
aOptions->fork_flags = flags;
}
#if defined(MOZ_ENABLE_FORKSERVER)
/**
* Called by the fork server to install a fork delegator.
*
* In the case of fork server, the value of the flags of |SandboxFork|
* are passed as an env variable to the fork server so that we can
* recreate a |SandboxFork| as a fork delegator at the fork server.
*/
void SandboxLaunchForkServerPrepare(const std::vector<std::string>& aArgv,
base::LaunchOptions& aOptions) {
auto chroot = std::find_if(
aOptions.env_map.begin(), aOptions.env_map.end(),
[](auto& elt) { return elt.first == kSandboxChrootEnvFlag; });
if (chroot == aOptions.env_map.end()) {
return;
}
bool canChroot = chroot->second.c_str()[0] == '1';
int flags = atoi(chroot->second.c_str() + 1);
MOZ_ASSERT(flags || canChroot);
SandboxLaunch::SandboxLaunch()
: mFlags(0), mChrootServer(-1), mChrootClient(-1) {}
// Find chroot server fd. It is supposed to be map to
// kSandboxChrootServerFd so that we find it out from the mapping.
auto fdmap = std::find_if(
aOptions.fds_to_remap.begin(), aOptions.fds_to_remap.end(),
[](auto& elt) { return elt.second == kSandboxChrootServerFd; });
MOZ_ASSERT(fdmap != aOptions.fds_to_remap.end(),
"ChrootServerFd is not found with sandbox chroot");
int chrootserverfd = fdmap->first;
aOptions.fds_to_remap.erase(fdmap);
// Set only the chroot server fd, not the client fd. Because, the
// client fd is already in |fds_to_remap|, we don't need the forker
// to do it again. And, the forker need only the server fd, that
// chroot server uses it to sync with the client (content). See
// |SandboxFox::StartChrootServer()|.
auto forker = MakeUnique<SandboxFork>(flags, canChroot, chrootserverfd);
aOptions.fork_delegate = std::move(forker);
}
#endif
SandboxFork::SandboxFork(int aFlags, bool aChroot, int aServerFd, int aClientFd)
: mFlags(aFlags), mChrootServer(aServerFd), mChrootClient(aClientFd) {
if (aChroot && mChrootServer < 0) {
int fds[2];
int rv = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds);
if (rv != 0) {
SANDBOX_LOG_ERRNO("socketpair");
MOZ_CRASH("socketpair failed");
}
mChrootClient = fds[0];
mChrootServer = fds[1];
}
}
void SandboxFork::PrepareMapping(base::file_handle_mapping_vector* aMap) {
MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_ForkServer);
if (mChrootClient >= 0) {
aMap->push_back({mChrootClient, kSandboxChrootClientFd});
}
#if defined(MOZ_ENABLE_FORKSERVER)
if (mChrootServer >= 0) {
aMap->push_back({mChrootServer, kSandboxChrootServerFd});
}
#endif
}
SandboxFork::~SandboxFork() {
SandboxLaunch::~SandboxLaunch() {
if (mChrootClient >= 0) {
close(mChrootClient);
}
@ -448,6 +369,31 @@ SandboxFork::~SandboxFork() {
}
}
bool SandboxLaunch::Prepare(LaunchOptions* aOptions) {
MOZ_ASSERT(mChrootClient < 0 && mChrootServer < 0);
mFlags = aOptions->fork_flags;
// Create the socket for communication between the child process and
// the chroot helper process. The client end is passed to the child
// via `fds_to_remap` and the server end is inherited and used in
// `StartChrootServer`.
if (aOptions->sandbox_chroot) {
int fds[2];
int rv = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds);
if (rv != 0) {
SANDBOX_LOG_ERRNO("socketpair");
return false;
}
mChrootClient = fds[0];
mChrootServer = fds[1];
aOptions->fds_to_remap.push_back({mChrootClient, kSandboxChrootClientFd});
}
return true;
}
static void BlockAllSignals(sigset_t* aOldSigs) {
sigset_t allSigs;
int rv = sigfillset(&allSigs);
@ -613,7 +559,7 @@ static void DropAllCaps() {
}
}
pid_t SandboxFork::Fork() {
pid_t SandboxLaunch::Fork() {
if (mFlags == 0) {
MOZ_ASSERT(mChrootServer < 0);
return fork();
@ -649,16 +595,28 @@ pid_t SandboxFork::Fork() {
if (mChrootServer >= 0) {
StartChrootServer();
// Don't close the client fd when this object is destroyed. At
// this point we're in the child process proper, so it's "owned"
// by the FileDescriptorShuffle / CloseSuperfluous code (i.e.,
// that's what will consume it and close it).
mChrootClient = -1;
}
// execve() will drop capabilities, but it seems best to also drop
// them here in case they'd do something unexpected in the generic
// post-fork code.
// execve() will drop capabilities, but the fork server case doesn't
// exec so we need to do this directly. (Also, it's a good idea to
// follow the principle of least privilege even when not strictly
// necessary.)
//
// Note that, while capabilities within an unprivileged user
// namespace are constrained in theory, in practice they expose a
// lot of attack surface and there have been exploitable kernel bugs
// related to that in the past, so we really want to drop them
// before doing anything that needs sandboxing.
DropAllCaps();
return 0;
}
void SandboxFork::StartChrootServer() {
void SandboxLaunch::StartChrootServer() {
// Run the rest of this function in a separate process that can
// chroot() on behalf of this process after it's sandboxed.
pid_t pid = ForkWithFlags(CLONE_FS);

Просмотреть файл

@ -14,15 +14,56 @@
namespace mozilla {
// Called in the parent process to set up launch-time aspects of
// sandboxing. If aType is GeckoProcessType_Content, this must be
// called on the main thread in order to access prefs.
void SandboxLaunchPrepare(GeckoProcessType aType, base::LaunchOptions* aOptions,
ipc::SandboxingKind aKind);
#if defined(MOZ_ENABLE_FORKSERVER)
void SandboxLaunchForkServerPrepare(const std::vector<std::string>& aArgv,
base::LaunchOptions& aOptions);
#endif
class SandboxLaunch final {
public:
SandboxLaunch();
~SandboxLaunch();
SandboxLaunch(const SandboxLaunch&) = delete;
SandboxLaunch& operator=(const SandboxLaunch&) = delete;
using LaunchOptions = base::LaunchOptions;
using SandboxingKind = ipc::SandboxingKind;
// Decide what sandboxing features will be used for a process, and
// modify `*aOptions` accordingly. This does not allocate fds or
// other OS resources (other than memory for strings).
//
// This is meant to be called in the parent process (even if the
// fork server will be used), and if `aType` is Content then it must
// be called on the main thread in order to access prefs.
static void Configure(GeckoProcessType aType, SandboxingKind aKind,
LaunchOptions* aOptions);
// Finish setting up for process launch, based on the information
// from `Configure(...)`. Called in the process that will do the
// launch (fork server if applicable, otherwise parent), and before
// calling `FileDescriptorShuffle::Init`.
//
// This can allocate fds (owned by `*this`) and modify
// `aOptions->fds_to_remap`, but does not access the
// environment-related fields of `*aOptions`.
bool Prepare(LaunchOptions* aOptions);
// Launch the child process, similarly to `::fork()`; called after
// `Configure` and `Prepare`.
//
// If launch-time sandboxing features are used, `pthread_atfork`
// hooks are not currently supported in that case, and signal
// handlers are reset in the child process. If sandboxing is not
// used, this is equivalent to `::fork()`.
pid_t Fork();
private:
int mFlags;
int mChrootServer;
int mChrootClient;
void StartChrootServer();
};
// This doesn't really belong in this header but it's used in both
// SandboxLaunch and SandboxBrokerPolicyFactory.
bool HasAtiDrivers();
} // namespace mozilla