Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/sentry/kernel/kernel.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@ type Kernel struct {
// IOUringEnabled determines if io_uring is enabled.
IOUringEnabled bool

// RestrictBindToLoopback, when true, causes bind(2) to return EACCES for
// any non-loopback address (including INADDR_ANY / IN6ADDR_ANY).
RestrictBindToLoopback bool

// MaxKeySetSize is the maximum number of keys in a key set.
MaxKeySetSize atomicbitops.Int32

Expand Down
27 changes: 27 additions & 0 deletions pkg/sentry/syscalls/linux/sys_socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package linux

import (
"fmt"
"net"
"time"

"golang.org/x/sys/unix"
Expand Down Expand Up @@ -371,6 +372,32 @@ func Bind(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *
return 0, nil, err
}

// Enforce restrict-bind-to-loopback policy: reject any bind to a
// non-loopback address (including INADDR_ANY / IN6ADDR_ANY) so that
// sandboxed processes cannot inadvertently expose services on external
// interfaces.
if t.Kernel().RestrictBindToLoopback && len(a) >= 2 {
family := hostarch.ByteOrder.Uint16(a)
switch int(family) {
case linux.AF_INET:
if len(a) >= linux.SockAddrInetSize {
var sa linux.SockAddrInet
sa.UnmarshalUnsafe(a)
if !net.IP(sa.Addr[:]).IsLoopback() {
return 0, nil, linuxerr.EACCES
}
}
case linux.AF_INET6:
if len(a) >= linux.SockAddrInet6Size {
var sa linux.SockAddrInet6
sa.UnmarshalUnsafe(a)
if !net.IP(sa.Addr[:]).IsLoopback() {
return 0, nil, linuxerr.EACCES
}
}
}
}

return 0, nil, s.Bind(t, a).ToError()
}

Expand Down
9 changes: 5 additions & 4 deletions runsc/boot/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,10 +634,11 @@ func New(args Args) (*Loader, error) {
}
}
l.k = &kernel.Kernel{
Platform: p,
NvidiaDriverVersion: args.NvidiaDriverVersion,
AllowSUID: args.Conf.AllowSUID,
IOUringEnabled: args.Conf.IOUring,
Platform: p,
NvidiaDriverVersion: args.NvidiaDriverVersion,
AllowSUID: args.Conf.AllowSUID,
IOUringEnabled: args.Conf.IOUring,
RestrictBindToLoopback: args.Conf.RestrictBindToLoopback,
}

// Create memory file.
Expand Down
6 changes: 6 additions & 0 deletions runsc/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,12 @@ type Config struct {
// and can be unpaused manually.
PauseExternalNetworking bool `flag:"pause-external-networking"`

// RestrictBindToLoopback, when true, causes bind(2) to return EACCES for
// any address that is not a loopback address (127.x.x.x or ::1), including
// INADDR_ANY (0.0.0.0) and IN6ADDR_ANY (::). Only meaningful with sandbox
// networking.
RestrictBindToLoopback bool `flag:"restrict-bind-to-loopback"`

// AllowConnectedOnSave allows network connections to stay established on save.
AllowConnectedOnSave bool `flag:"allow-connected-on-save"`

Expand Down
29 changes: 29 additions & 0 deletions runsc/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,35 @@ func TestToFlagsFromManual(t *testing.T) {
}
}

// TestRestrictBindToLoopback verifies that the restrict-bind-to-loopback flag
// round-trips correctly through RegisterFlags/NewFromFlags/ToFlags.
func TestRestrictBindToLoopback(t *testing.T) {
testFlags := flag.NewFlagSet("test", flag.ContinueOnError)
RegisterFlags(testFlags)
if err := testFlags.Lookup("restrict-bind-to-loopback").Value.Set("true"); err != nil {
t.Fatalf("setting restrict-bind-to-loopback: %v", err)
}
c, err := NewFromFlags(testFlags)
if err != nil {
t.Fatal(err)
}
if !c.RestrictBindToLoopback {
t.Error("expected RestrictBindToLoopback=true after setting flag, got false")
}

flags := c.ToFlags()
found := false
for _, f := range flags {
if f == "--restrict-bind-to-loopback=true" {
found = true
break
}
}
if !found {
t.Errorf("--restrict-bind-to-loopback=true not in ToFlags() output: %v", flags)
}
}

// TestInvalidFlags checks that enum flags fail when value is not in enum set.
func TestInvalidFlags(t *testing.T) {
for _, tc := range []struct {
Expand Down
1 change: 1 addition & 0 deletions runsc/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ func RegisterFlags(flagSet *flag.FlagSet) {
flagSet.Bool(flagReproduceNFTables, false, "Attempt to scrape and reproduce nftable rules inside the sandbox. Overrides reproduce-nat when true.")
flagSet.Bool(flagNetDisconnectOK, true, "Indicates whether open network connections and open unix domain sockets should be disconnected upon save.")
flagSet.Bool(flagPauseExternalNetworking, false, "Start the sandbox with external networking disabled. Only supported when using the sandbox network type. The network can be unpaused manually after the sandbox is running.")
flagSet.Bool("restrict-bind-to-loopback", false, "Reject bind(2) calls to non-loopback addresses (including INADDR_ANY/::). Sandboxed processes may only bind to 127.x.x.x or ::1. Only meaningful with sandbox networking.")
flagSet.Bool(flagAllowConnectedOnSave, false, "Allow network connections to stay established on save.")

// Flags that control sandbox runtime behavior: accelerator related.
Expand Down
14 changes: 14 additions & 0 deletions test/syscalls/linux/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2378,6 +2378,20 @@ cc_binary(
],
)

cc_binary(
name = "restrict_bind_loopback_test",
testonly = 1,
srcs = ["restrict_bind_loopback.cc"],
linkstatic = 1,
malloc = "//test/util:errno_safe_allocator",
deps = select_gtest() + [
"//test/util:file_descriptor",
"//test/util:socket_util",
"//test/util:test_main",
"//test/util:test_util",
],
)

cc_binary(
name = "rename_test",
testonly = 1,
Expand Down
181 changes: 181 additions & 0 deletions test/syscalls/linux/restrict_bind_loopback.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright 2025 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Tests for the --restrict-bind-to-loopback runsc flag. When the flag is
// enabled, bind(2) must return EACCES for any address that is not a loopback
// address (127.x.x.x or ::1). These tests skip automatically when the flag is
// not active.

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

#include "gtest/gtest.h"
#include "test/util/file_descriptor.h"
#include "test/util/socket_util.h"
#include "test/util/test_util.h"

namespace gvisor {
namespace testing {

namespace {

// Detect whether --restrict-bind-to-loopback is active by probing a bind to
// INADDR_ANY. On a normal kernel or a gVisor instance without the flag, this
// bind will succeed (or fail with something other than EACCES).
bool RestrictBindToLoopbackEnabled() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return false;
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret =
bind(sock, reinterpret_cast<const struct sockaddr*>(&addr), sizeof(addr));
int saved_errno = errno;
close(sock);
return ret < 0 && saved_errno == EACCES;
}

// IPv4: binding to INADDR_ANY (0.0.0.0) must fail with EACCES.
TEST(RestrictBindLoopback, IPv4_INADDR_ANY_Rejected) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallFailsWithErrno(EACCES));
}

// IPv4: binding to a non-loopback unicast address must fail with EACCES.
TEST(RestrictBindLoopback, IPv4_NonLoopback_Rejected) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
inet_pton(AF_INET, "192.0.2.1", &addr.sin_addr); // TEST-NET-1, RFC 5737
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallFailsWithErrno(EACCES));
}

// IPv4: binding to 127.0.0.1 must succeed.
TEST(RestrictBindLoopback, IPv4_Loopback_Allowed) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = 0;
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallSucceeds());
}

// IPv4: binding to 127.x.x.x (any loopback block address) must succeed.
TEST(RestrictBindLoopback, IPv4_LoopbackBlock_Allowed) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = 0;
inet_pton(AF_INET, "127.0.0.2", &addr.sin_addr);
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallSucceeds());
}

// IPv6: binding to IN6ADDR_ANY (::) must fail with EACCES.
TEST(RestrictBindLoopback, IPv6_IN6ADDR_ANY_Rejected) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
struct sockaddr_in6 addr = {};
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallFailsWithErrno(EACCES));
}

// IPv6: binding to ::1 must succeed.
TEST(RestrictBindLoopback, IPv6_Loopback_Allowed) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
struct sockaddr_in6 addr = {};
addr.sin6_family = AF_INET6;
addr.sin6_port = 0;
addr.sin6_addr = in6addr_loopback;
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallSucceeds());
}

// IPv6: binding to ::ffff:127.0.0.1 (IPv4-mapped loopback) must succeed.
// Dual-stack sockets can bind to IPv4 addresses in mapped form; rejecting this
// would break applications that use IPv6 sockets for both IPv4 and IPv6.
TEST(RestrictBindLoopback, IPv6_IPv4MappedLoopback_Allowed) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
struct sockaddr_in6 addr = {};
addr.sin6_family = AF_INET6;
addr.sin6_port = 0;
// ::ffff:127.0.0.1
inet_pton(AF_INET6, "::ffff:127.0.0.1", &addr.sin6_addr);
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallSucceeds());
}

// IPv6: binding to ::ffff:0.0.0.0 (IPv4-mapped INADDR_ANY) must fail.
TEST(RestrictBindLoopback, IPv6_IPv4MappedAny_Rejected) {
SKIP_IF(!RestrictBindToLoopbackEnabled());

FileDescriptor sock =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
struct sockaddr_in6 addr = {};
addr.sin6_family = AF_INET6;
// ::ffff:0.0.0.0
inet_pton(AF_INET6, "::ffff:0.0.0.0", &addr.sin6_addr);
EXPECT_THAT(
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
sizeof(addr)),
SyscallFailsWithErrno(EACCES));
}

} // namespace

} // namespace testing
} // namespace gvisor