Skip to content

Commit

Permalink
Implement ForkServer-based shared network namespace
Browse files Browse the repository at this point in the history
Namespace creation comes at a price. We introduce a means to tie a netns to a forkserver. This means any sandbox started by that forkserver process will have the same netns.

PiperOrigin-RevId: 684800270
Change-Id: Ife982bd7bad22ccec9f7fc20b3f127c87622f18d
  • Loading branch information
okunz authored and copybara-github committed Oct 11, 2024
1 parent 9e07542 commit a7ad546
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 19 deletions.
4 changes: 3 additions & 1 deletion sandboxed_api/sandbox2/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ cc_library(
":allow_all_syscalls",
":allow_seccomp_speculation",
":allow_unrestricted_networking",
":forkserver_cc_proto",
":mounts",
":namespace",
":policy",
Expand Down Expand Up @@ -745,6 +746,7 @@ cc_library(
hdrs = ["namespace.h"],
copts = sapi_platform_copts(),
deps = [
":forkserver_cc_proto",
":mounts",
":violation_cc_proto",
"//sandboxed_api/util:file_base",
Expand All @@ -769,12 +771,12 @@ cc_test(
":sandbox2",
":testonly_allow_all_syscalls",
":testonly_allow_unrestricted_networking",
"//sandboxed_api:config",
"//sandboxed_api:testing",
"//sandboxed_api/util:fileops",
"//sandboxed_api/util:status_matchers",
"//sandboxed_api/util:temp_file",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_googletest//:gtest_main",
Expand Down
6 changes: 4 additions & 2 deletions sandboxed_api/sandbox2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ target_link_libraries(sandbox2_policybuilder
absl::span
absl::strings
absl::statusor
sandbox2::forkserver_proto
sandbox2::mounts
sandbox2::network_proxy_filtering
sandbox2::policy
Expand Down Expand Up @@ -671,7 +672,8 @@ target_link_libraries(sandbox2_namespace
sapi::fileops
sapi::base
sapi::raw_logging
PUBLIC sandbox2::violation_proto
PUBLIC sandbox2::forkserver_proto
sandbox2::violation_proto
sandbox2::mounts
)

Expand Down Expand Up @@ -880,11 +882,11 @@ if(BUILD_TESTING AND SAPI_BUILD_TESTING)
)
target_link_libraries(sandbox2_namespace_test PRIVATE
absl::check
absl::status
absl::statusor
absl::strings
sandbox2::allow_all_syscalls
sandbox2::allow_unrestricted_networking
sapi::config
sapi::fileops
sandbox2::namespace
sandbox2::sandbox2
Expand Down
1 change: 1 addition & 0 deletions sandboxed_api/sandbox2/executor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ absl::StatusOr<SandboxeeProcess> Executor::StartSubProcess(

if (ns) {
clone_flags |= ns->clone_flags();
request.set_netns_mode(ns->netns_config());
*request.mutable_mount_tree() = ns->mounts().GetMountTree();
request.set_hostname(ns->hostname());
request.set_allow_mount_propagation(ns->allow_mount_propagation());
Expand Down
11 changes: 10 additions & 1 deletion sandboxed_api/sandbox2/forkserver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ pid_t ForkServer::ServeRequest() {
"joining initial user namespace");
SAPI_RAW_PCHECK(setns(initial_mntns_fd_, CLONE_NEWNS) != -1,
"joining initial mnt namespace");
if (fork_request.netns_mode() == NETNS_MODE_SHARED_PER_FORKSERVER) {
SAPI_RAW_PCHECK(setns(initial_netns_fd_, CLONE_NEWNET) != -1,
"joining initial net namespace");
close(initial_netns_fd_);
}
close(initial_userns_fd_);
close(initial_mntns_fd_);
// Do not create new userns it will be unshared later
Expand Down Expand Up @@ -600,7 +605,8 @@ void ForkServer::CreateInitialNamespaces() {
SAPI_RAW_PCHECK(create_efd.get() != -1, "creating eventfd");
FDCloser open_efd(eventfd(0, EFD_CLOEXEC));
SAPI_RAW_PCHECK(open_efd.get() != -1, "creating eventfd");
pid_t pid = util::ForkWithFlags(CLONE_NEWUSER | CLONE_NEWNS | SIGCHLD);
pid_t pid =
util::ForkWithFlags(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD);
if (pid == -1 && errno == EPERM && IsLikelyChrooted()) {
SAPI_RAW_LOG(FATAL,
"failed to fork initial namespaces process: parent process is "
Expand Down Expand Up @@ -630,6 +636,9 @@ void ForkServer::CreateInitialNamespaces() {
initial_mntns_fd_ = open(absl::StrCat("/proc/", pid, "/ns/mnt").c_str(),
O_RDONLY | O_CLOEXEC);
SAPI_RAW_PCHECK(initial_mntns_fd_ != -1, "getting initial mntns fd");
initial_netns_fd_ = open(absl::StrCat("/proc/", pid, "/ns/net").c_str(),
O_RDONLY | O_CLOEXEC);
SAPI_RAW_PCHECK(initial_netns_fd_ != -1, "getting initial netns fd");
SAPI_RAW_PCHECK(TEMP_FAILURE_RETRY(write(open_efd.get(), &value,
sizeof(value))) == sizeof(value),
"synchronizing initial namespaces creation");
Expand Down
1 change: 1 addition & 0 deletions sandboxed_api/sandbox2/forkserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class ForkServer {
Comms* comms_;
int initial_mntns_fd_ = -1;
int initial_userns_fd_ = -1;
int initial_netns_fd_ = -1;
};

} // namespace sandbox2
Expand Down
23 changes: 23 additions & 0 deletions sandboxed_api/sandbox2/forkserver.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ enum MonitorType {
FORKSERVER_MONITOR_UNOTIFY = 2;
}

// Enum representing the net_ns mode, used by policybuilder, forkserver, and
// executor.
enum NetNsMode {
NETNS_MODE_UNSPECIFIED = 0;

// Create a new netns for each sandbox (default).
NETNS_MODE_PER_SANDBOXEE = 1;

// Do not create a netns.
// This will disable the network namespace isolation
// from the host and expose its network interfaces to the sandboxee (generally
// granting internet access).
// Networking syscalls must be allowed in the policy in order to use the
// network.
NETNS_MODE_NONE = 2;

// Create a netns shared by all sandboxees started by a forkserver.
NETNS_MODE_SHARED_PER_FORKSERVER = 3;
}

message ForkRequest {
// List of arguments, starting with argv[0]
repeated bytes args = 1;
Expand Down Expand Up @@ -69,4 +89,7 @@ message ForkRequest {

// Whether to allow speculative execution inside the sandboxee
optional bool allow_speculation = 10;

// Net namespace mode
optional NetNsMode netns_mode = 11;
}
14 changes: 10 additions & 4 deletions sandboxed_api/sandbox2/namespace.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include <vector>

#include "absl/strings/str_cat.h"
#include "sandboxed_api/sandbox2/forkserver.pb.h"
#include "sandboxed_api/sandbox2/mounts.h"
#include "sandboxed_api/sandbox2/violation.pb.h"
#include "sandboxed_api/util/fileops.h"
#include "sandboxed_api/util/path.h"
Expand Down Expand Up @@ -195,12 +197,16 @@ void LogFilesystem(const std::string& dir) {

} // namespace

Namespace::Namespace(bool allow_unrestricted_networking, Mounts mounts,
std::string hostname, bool allow_mount_propagation)
Namespace::Namespace(Mounts mounts, std::string hostname,
NetNsMode netns_config, bool allow_mount_propagation)
: mounts_(std::move(mounts)),
hostname_(std::move(hostname)),
allow_mount_propagation_(allow_mount_propagation) {
if (allow_unrestricted_networking) {
allow_mount_propagation_(allow_mount_propagation),
netns_config_(netns_config) {
// Remove the CLONE_NEWNET flag to allow networking, or for the shared netns.
// In the latter case, the flag will be added later on.
if (netns_config_ == NETNS_MODE_NONE ||
netns_config_ == NETNS_MODE_SHARED_PER_FORKSERVER) {
clone_flags_ &= ~CLONE_NEWNET;
}
}
Expand Down
10 changes: 7 additions & 3 deletions sandboxed_api/sandbox2/namespace.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <cstdint>
#include <string>

#include "sandboxed_api/sandbox2/forkserver.pb.h"
#include "sandboxed_api/sandbox2/mounts.h"
#include "sandboxed_api/sandbox2/violation.pb.h"

Expand All @@ -39,12 +40,14 @@ class Namespace final {
bool allow_mount_propagation);
static void InitializeInitialNamespaces(uid_t uid, gid_t gid);

Namespace(bool allow_unrestricted_networking, Mounts mounts,
std::string hostname, bool allow_mount_propagation);
Namespace(Mounts mounts, std::string hostname, NetNsMode netns_config,
bool allow_mount_propagation = false);

// Stores information about this namespace in the protobuf structure.
void GetNamespaceDescription(NamespaceDescription* pb_description) const;

NetNsMode netns_config() const { return netns_config_; }

int32_t clone_flags() const { return clone_flags_; }

Mounts& mounts() { return mounts_; }
Expand All @@ -59,7 +62,8 @@ class Namespace final {
CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;
Mounts mounts_;
std::string hostname_;
bool allow_mount_propagation_ = false;
bool allow_mount_propagation_;
NetNsMode netns_config_;
};

} // namespace sandbox2
Expand Down
58 changes: 57 additions & 1 deletion sandboxed_api/sandbox2/namespace_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#include "sandboxed_api/sandbox2/namespace.h"
#include "sandboxed_api/sandbox2/namespace.h" // IWYU pragma: keep

#include <asm-generic/unistd.h>
#include <unistd.h>

#include <cstdint>
Expand All @@ -27,6 +28,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
Expand Down Expand Up @@ -255,6 +257,60 @@ TEST(NamespaceTest, TestInterfacesWithNetwork) {
EXPECT_THAT(result, SizeIs(Gt(1)));
}

TEST(NamespaceTest, TestNetNsModeForkServerShared) {
constexpr uint32_t kReadlink[] = {
#ifdef __NR_readlink
__NR_readlink,
#endif
__NR_readlinkat};

std::unique_ptr<sandbox2::Policy> policy;
const std::string path = GetTestcaseBinPath("namespace");
std::initializer_list<std::string> args = {path, "8"};

// Sandbox2 run without a ForkServer shared net_ns
SAPI_ASSERT_OK_AND_ASSIGN(policy, CreateDefaultPermissiveTestPolicy(path)
.AllowSyscalls(kReadlink)
.AddDirectory("/proc")
.TryBuild());
std::vector<std::string> result_individual_netns_run =
RunSandboxeeWithArgsAndPolicy(path, args, std::move(policy));
EXPECT_THAT(result_individual_netns_run, SizeIs(1));

// Two Sandbox2 runs with a ForkServer shared net_ns
SAPI_ASSERT_OK_AND_ASSIGN(policy, CreateDefaultPermissiveTestPolicy(path)
.AllowSyscalls(kReadlink)
.AddDirectory("/proc")
.UseForkServerSharedNetNs()
.TryBuild());
std::vector<std::string> result_one =
RunSandboxeeWithArgsAndPolicy(path, args, std::move(policy));
EXPECT_THAT(result_one.size(), Eq(1));

SAPI_ASSERT_OK_AND_ASSIGN(policy, CreateDefaultPermissiveTestPolicy(path)
.AllowSyscalls(kReadlink)
.AddDirectory("/proc")
.UseForkServerSharedNetNs()
.TryBuild());
std::vector<std::string> result_two =
RunSandboxeeWithArgsAndPolicy(path, args, std::move(policy));
EXPECT_THAT(result_two.size(), Eq(1));

EXPECT_THAT(result_one, Eq(result_two));
EXPECT_THAT(result_one, Ne(result_individual_netns_run));
EXPECT_THAT(result_two, Ne(result_individual_netns_run));
}

TEST(NamespaceTest, TestIncompatibleNetNsModes) {
const std::string path = GetTestcaseBinPath("namespace");
auto policy = CreateDefaultPermissiveTestPolicy(path)
.Allow(UnrestrictedNetworking())
.UseForkServerSharedNetNs()
.TryBuild();
EXPECT_THAT(policy.status(),
sapi::StatusIs(absl::StatusCode::kFailedPrecondition));
}

TEST(NamespaceTest, TestFiles) {
SKIP_ANDROID;
const std::string path = GetTestcaseBinPath("namespace");
Expand Down
38 changes: 33 additions & 5 deletions sandboxed_api/sandbox2/policybuilder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
#include "sandboxed_api/sandbox2/allow_all_syscalls.h"
#include "sandboxed_api/sandbox2/allow_seccomp_speculation.h"
#include "sandboxed_api/sandbox2/allow_unrestricted_networking.h"
#include "sandboxed_api/sandbox2/forkserver.pb.h"
#include "sandboxed_api/sandbox2/namespace.h"
#include "sandboxed_api/sandbox2/network_proxy/filtering.h"
#include "sandboxed_api/sandbox2/policy.h"
Expand Down Expand Up @@ -126,7 +127,16 @@ bool IsOnReadOnlyDev(const std::string& path) {

PolicyBuilder& PolicyBuilder::Allow(UnrestrictedNetworking) {
EnableNamespaces(); // NOLINT(clang-diagnostic-deprecated-declarations)
allow_unrestricted_networking_ = true;

if (netns_mode_ != NETNS_MODE_UNSPECIFIED) {
SetError(absl::FailedPreconditionError(absl::StrCat(
"Incompatible with other network namespaces modes. A sandbox can have "
"only one network namespace mode. Attempted to configure: ",
NetNsMode_Name(netns_mode_))));
return *this;
}

netns_mode_ = NETNS_MODE_NONE;
return *this;
}

Expand Down Expand Up @@ -1447,13 +1457,16 @@ absl::StatusOr<std::unique_ptr<Policy>> PolicyBuilder::TryBuild() {
}

if (use_namespaces_) {
if (allow_unrestricted_networking_ && hostname_ != kDefaultHostname) {
// If no specific netns mode is set, default to per-sandboxee.
if (netns_mode_ == NETNS_MODE_UNSPECIFIED) {
netns_mode_ = NETNS_MODE_PER_SANDBOXEE;
}
if (netns_mode_ == NETNS_MODE_NONE && hostname_ != kDefaultHostname) {
return absl::FailedPreconditionError(
"Cannot set hostname without network namespaces.");
}
policy->namespace_ =
Namespace(allow_unrestricted_networking_, std::move(mounts_), hostname_,
allow_mount_propagation_);
policy->namespace_ = Namespace(std::move(mounts_), hostname_, netns_mode_,
allow_mount_propagation_);
}

policy->allow_speculation_ = allow_speculation_;
Expand Down Expand Up @@ -1619,6 +1632,21 @@ PolicyBuilder& PolicyBuilder::AllowUnrestrictedNetworking() {
return Allow(UnrestrictedNetworking());
}

PolicyBuilder& PolicyBuilder::UseForkServerSharedNetNs() {
EnableNamespaces(); // NOLINT(clang-diagnostic-deprecated-declarations)

if (netns_mode_ != NETNS_MODE_UNSPECIFIED) {
SetError(absl::FailedPreconditionError(absl::StrCat(
"Incompatible with other network namespaces modes. A sandbox can have "
"only one network namespace mode. Attempted to configure: ",
NetNsMode_Name(netns_mode_))));
return *this;
}

netns_mode_ = NETNS_MODE_SHARED_PER_FORKSERVER;
return *this;
}

PolicyBuilder& PolicyBuilder::SetHostname(absl::string_view hostname) {
EnableNamespaces(); // NOLINT(clang-diagnostic-deprecated-declarations)
hostname_ = std::string(hostname);
Expand Down
13 changes: 12 additions & 1 deletion sandboxed_api/sandbox2/policybuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "absl/types/span.h"
#include "sandboxed_api/sandbox2/forkserver.pb.h"
#include "sandboxed_api/sandbox2/mounts.h"
#include "sandboxed_api/sandbox2/network_proxy/filtering.h"
#include "sandboxed_api/sandbox2/policy.h"
Expand Down Expand Up @@ -695,6 +696,16 @@ class PolicyBuilder final {
ABSL_DEPRECATED("Use Allow(sandbox2::UnrestrictedNetworking()) instead.")
PolicyBuilder& AllowUnrestrictedNetworking();

// Enables a shared network namespace for all sandboxees that are started by
// the same forkserver.
//
// This results in sandboxed processes to run in the same shared network
// namespace instead of creating a separate network namespace for each
// sandboxed process started by the ForkServer process.
//
// IMPORTANT: This is incompatible with AllowUnrestrictedNetworking.
PolicyBuilder& UseForkServerSharedNetNs();

// Enables the use of namespaces.
//
// Namespaces are enabled by default.
Expand Down Expand Up @@ -834,7 +845,7 @@ class PolicyBuilder final {
Mounts mounts_;
bool use_namespaces_ = true;
bool requires_namespaces_ = false;
bool allow_unrestricted_networking_ = false;
NetNsMode netns_mode_ = NETNS_MODE_UNSPECIFIED;
bool allow_speculation_ = false;
bool allow_mount_propagation_ = false;
std::string hostname_ = std::string(kDefaultHostname);
Expand Down
1 change: 1 addition & 0 deletions sandboxed_api/sandbox2/policybuilder_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ TEST(PolicyBuilderTest, Testpolicy_size) {
builder.AddFile("/usr/bin/find"); assert_same();
builder.AddDirectory("/bin"); assert_same();
builder.AddTmpfs("/tmp", /*size=*/4ULL << 20 /* 4 MiB */); assert_same();
builder.UseForkServerSharedNetNs(); assert_same();
builder.Allow(UnrestrictedNetworking()); assert_same();
// clang-format on
}
Expand Down
Loading

0 comments on commit a7ad546

Please sign in to comment.