Skip to content

Commit 3d4d9e7

Browse files
runsc: add --restrict-bind-to-loopback flag
When enabled, bind(2) returns EACCES for any address that is not a loopback address, including INADDR_ANY (0.0.0.0) and IN6ADDR_ANY (::). Loopback classification is delegated to net.IP.IsLoopback() from the Go standard library. AF_UNIX and other address families are unaffected. Thread safety: RestrictBindToLoopback is written once during sandbox boot (loader.go:New) before any task goroutines are created, and is read-only thereafter — the same pattern used by AllowSUID and IOUringEnabled. The flag is only meaningful with sandbox networking (--network=sandbox). With host networking the sentry syscall handlers are not invoked for socket calls, so the check is a no-op. Tests: - runsc/config: TestRestrictBindToLoopback verifies flag round-trips through RegisterFlags/NewFromFlags/ToFlags. - test/syscalls/linux/restrict_bind_loopback_test: eight cases covering IPv4 INADDR_ANY (rejected), non-loopback unicast (rejected), 127.0.0.1 / 127.x.x.x (allowed), IPv6 IN6ADDR_ANY (rejected), ::1 (allowed), ::ffff:127.0.0.1 (allowed), and ::ffff:0.0.0.0 (rejected). Tests skip automatically when the flag is not active, so they are safe to run on plain Linux or gVisor without the flag. Assisted-by: Claude Sonnet 4.6
1 parent abf5927 commit 3d4d9e7

8 files changed

Lines changed: 267 additions & 4 deletions

File tree

pkg/sentry/kernel/kernel.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,10 @@ type Kernel struct {
433433
// IOUringEnabled determines if io_uring is enabled.
434434
IOUringEnabled bool
435435

436+
// RestrictBindToLoopback, when true, causes bind(2) to return EACCES for
437+
// any non-loopback address (including INADDR_ANY / IN6ADDR_ANY).
438+
RestrictBindToLoopback bool
439+
436440
// MaxKeySetSize is the maximum number of keys in a key set.
437441
MaxKeySetSize atomicbitops.Int32
438442

pkg/sentry/syscalls/linux/sys_socket.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package linux
1616

1717
import (
1818
"fmt"
19+
"net"
1920
"time"
2021

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

375+
// Enforce restrict-bind-to-loopback policy: reject any bind to a
376+
// non-loopback address (including INADDR_ANY / IN6ADDR_ANY) so that
377+
// sandboxed processes cannot inadvertently expose services on external
378+
// interfaces.
379+
if t.Kernel().RestrictBindToLoopback && len(a) >= 2 {
380+
family := hostarch.ByteOrder.Uint16(a)
381+
switch int(family) {
382+
case linux.AF_INET:
383+
if len(a) >= linux.SockAddrInetSize {
384+
var sa linux.SockAddrInet
385+
sa.UnmarshalUnsafe(a)
386+
if !net.IP(sa.Addr[:]).IsLoopback() {
387+
return 0, nil, linuxerr.EACCES
388+
}
389+
}
390+
case linux.AF_INET6:
391+
if len(a) >= linux.SockAddrInet6Size {
392+
var sa linux.SockAddrInet6
393+
sa.UnmarshalUnsafe(a)
394+
if !net.IP(sa.Addr[:]).IsLoopback() {
395+
return 0, nil, linuxerr.EACCES
396+
}
397+
}
398+
}
399+
}
400+
374401
return 0, nil, s.Bind(t, a).ToError()
375402
}
376403

runsc/boot/loader.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -634,10 +634,11 @@ func New(args Args) (*Loader, error) {
634634
}
635635
}
636636
l.k = &kernel.Kernel{
637-
Platform: p,
638-
NvidiaDriverVersion: args.NvidiaDriverVersion,
639-
AllowSUID: args.Conf.AllowSUID,
640-
IOUringEnabled: args.Conf.IOUring,
637+
Platform: p,
638+
NvidiaDriverVersion: args.NvidiaDriverVersion,
639+
AllowSUID: args.Conf.AllowSUID,
640+
IOUringEnabled: args.Conf.IOUring,
641+
RestrictBindToLoopback: args.Conf.RestrictBindToLoopback,
641642
}
642643

643644
// Create memory file.

runsc/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,12 @@ type Config struct {
438438
// and can be unpaused manually.
439439
PauseExternalNetworking bool `flag:"pause-external-networking"`
440440

441+
// RestrictBindToLoopback, when true, causes bind(2) to return EACCES for
442+
// any address that is not a loopback address (127.x.x.x or ::1), including
443+
// INADDR_ANY (0.0.0.0) and IN6ADDR_ANY (::). Only meaningful with sandbox
444+
// networking.
445+
RestrictBindToLoopback bool `flag:"restrict-bind-to-loopback"`
446+
441447
// AllowConnectedOnSave allows network connections to stay established on save.
442448
AllowConnectedOnSave bool `flag:"allow-connected-on-save"`
443449

runsc/config/config_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,35 @@ func TestToFlagsFromManual(t *testing.T) {
179179
}
180180
}
181181

182+
// TestRestrictBindToLoopback verifies that the restrict-bind-to-loopback flag
183+
// round-trips correctly through RegisterFlags/NewFromFlags/ToFlags.
184+
func TestRestrictBindToLoopback(t *testing.T) {
185+
testFlags := flag.NewFlagSet("test", flag.ContinueOnError)
186+
RegisterFlags(testFlags)
187+
if err := testFlags.Lookup("restrict-bind-to-loopback").Value.Set("true"); err != nil {
188+
t.Fatalf("setting restrict-bind-to-loopback: %v", err)
189+
}
190+
c, err := NewFromFlags(testFlags)
191+
if err != nil {
192+
t.Fatal(err)
193+
}
194+
if !c.RestrictBindToLoopback {
195+
t.Error("expected RestrictBindToLoopback=true after setting flag, got false")
196+
}
197+
198+
flags := c.ToFlags()
199+
found := false
200+
for _, f := range flags {
201+
if f == "--restrict-bind-to-loopback=true" {
202+
found = true
203+
break
204+
}
205+
}
206+
if !found {
207+
t.Errorf("--restrict-bind-to-loopback=true not in ToFlags() output: %v", flags)
208+
}
209+
}
210+
182211
// TestInvalidFlags checks that enum flags fail when value is not in enum set.
183212
func TestInvalidFlags(t *testing.T) {
184213
for _, tc := range []struct {

runsc/config/flags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ func RegisterFlags(flagSet *flag.FlagSet) {
170170
flagSet.Bool(flagReproduceNFTables, false, "Attempt to scrape and reproduce nftable rules inside the sandbox. Overrides reproduce-nat when true.")
171171
flagSet.Bool(flagNetDisconnectOK, true, "Indicates whether open network connections and open unix domain sockets should be disconnected upon save.")
172172
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.")
173+
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.")
173174
flagSet.Bool(flagAllowConnectedOnSave, false, "Allow network connections to stay established on save.")
174175

175176
// Flags that control sandbox runtime behavior: accelerator related.

test/syscalls/linux/BUILD

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2378,6 +2378,20 @@ cc_binary(
23782378
],
23792379
)
23802380

2381+
cc_binary(
2382+
name = "restrict_bind_loopback_test",
2383+
testonly = 1,
2384+
srcs = ["restrict_bind_loopback.cc"],
2385+
linkstatic = 1,
2386+
malloc = "//test/util:errno_safe_allocator",
2387+
deps = select_gtest() + [
2388+
"//test/util:file_descriptor",
2389+
"//test/util:socket_util",
2390+
"//test/util:test_main",
2391+
"//test/util:test_util",
2392+
],
2393+
)
2394+
23812395
cc_binary(
23822396
name = "rename_test",
23832397
testonly = 1,
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright 2025 The gVisor Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Tests for the --restrict-bind-to-loopback runsc flag. When the flag is
16+
// enabled, bind(2) must return EACCES for any address that is not a loopback
17+
// address (127.x.x.x or ::1). These tests skip automatically when the flag is
18+
// not active.
19+
20+
#include <arpa/inet.h>
21+
#include <netinet/in.h>
22+
#include <sys/socket.h>
23+
24+
#include "gtest/gtest.h"
25+
#include "test/util/file_descriptor.h"
26+
#include "test/util/socket_util.h"
27+
#include "test/util/test_util.h"
28+
29+
namespace gvisor {
30+
namespace testing {
31+
32+
namespace {
33+
34+
// Detect whether --restrict-bind-to-loopback is active by probing a bind to
35+
// INADDR_ANY. On a normal kernel or a gVisor instance without the flag, this
36+
// bind will succeed (or fail with something other than EACCES).
37+
bool RestrictBindToLoopbackEnabled() {
38+
int sock = socket(AF_INET, SOCK_STREAM, 0);
39+
if (sock < 0) return false;
40+
struct sockaddr_in addr = {};
41+
addr.sin_family = AF_INET;
42+
addr.sin_addr.s_addr = htonl(INADDR_ANY);
43+
int ret =
44+
bind(sock, reinterpret_cast<const struct sockaddr*>(&addr), sizeof(addr));
45+
int saved_errno = errno;
46+
close(sock);
47+
return ret < 0 && saved_errno == EACCES;
48+
}
49+
50+
// IPv4: binding to INADDR_ANY (0.0.0.0) must fail with EACCES.
51+
TEST(RestrictBindLoopback, IPv4_INADDR_ANY_Rejected) {
52+
SKIP_IF(!RestrictBindToLoopbackEnabled());
53+
54+
FileDescriptor sock =
55+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
56+
struct sockaddr_in addr = {};
57+
addr.sin_family = AF_INET;
58+
addr.sin_addr.s_addr = htonl(INADDR_ANY);
59+
EXPECT_THAT(
60+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
61+
sizeof(addr)),
62+
SyscallFailsWithErrno(EACCES));
63+
}
64+
65+
// IPv4: binding to a non-loopback unicast address must fail with EACCES.
66+
TEST(RestrictBindLoopback, IPv4_NonLoopback_Rejected) {
67+
SKIP_IF(!RestrictBindToLoopbackEnabled());
68+
69+
FileDescriptor sock =
70+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
71+
struct sockaddr_in addr = {};
72+
addr.sin_family = AF_INET;
73+
inet_pton(AF_INET, "192.0.2.1", &addr.sin_addr); // TEST-NET-1, RFC 5737
74+
EXPECT_THAT(
75+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
76+
sizeof(addr)),
77+
SyscallFailsWithErrno(EACCES));
78+
}
79+
80+
// IPv4: binding to 127.0.0.1 must succeed.
81+
TEST(RestrictBindLoopback, IPv4_Loopback_Allowed) {
82+
SKIP_IF(!RestrictBindToLoopbackEnabled());
83+
84+
FileDescriptor sock =
85+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
86+
struct sockaddr_in addr = {};
87+
addr.sin_family = AF_INET;
88+
addr.sin_port = 0;
89+
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
90+
EXPECT_THAT(
91+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
92+
sizeof(addr)),
93+
SyscallSucceeds());
94+
}
95+
96+
// IPv4: binding to 127.x.x.x (any loopback block address) must succeed.
97+
TEST(RestrictBindLoopback, IPv4_LoopbackBlock_Allowed) {
98+
SKIP_IF(!RestrictBindToLoopbackEnabled());
99+
100+
FileDescriptor sock =
101+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_STREAM, 0));
102+
struct sockaddr_in addr = {};
103+
addr.sin_family = AF_INET;
104+
addr.sin_port = 0;
105+
inet_pton(AF_INET, "127.0.0.2", &addr.sin_addr);
106+
EXPECT_THAT(
107+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
108+
sizeof(addr)),
109+
SyscallSucceeds());
110+
}
111+
112+
// IPv6: binding to IN6ADDR_ANY (::) must fail with EACCES.
113+
TEST(RestrictBindLoopback, IPv6_IN6ADDR_ANY_Rejected) {
114+
SKIP_IF(!RestrictBindToLoopbackEnabled());
115+
116+
FileDescriptor sock =
117+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
118+
struct sockaddr_in6 addr = {};
119+
addr.sin6_family = AF_INET6;
120+
addr.sin6_addr = in6addr_any;
121+
EXPECT_THAT(
122+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
123+
sizeof(addr)),
124+
SyscallFailsWithErrno(EACCES));
125+
}
126+
127+
// IPv6: binding to ::1 must succeed.
128+
TEST(RestrictBindLoopback, IPv6_Loopback_Allowed) {
129+
SKIP_IF(!RestrictBindToLoopbackEnabled());
130+
131+
FileDescriptor sock =
132+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
133+
struct sockaddr_in6 addr = {};
134+
addr.sin6_family = AF_INET6;
135+
addr.sin6_port = 0;
136+
addr.sin6_addr = in6addr_loopback;
137+
EXPECT_THAT(
138+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
139+
sizeof(addr)),
140+
SyscallSucceeds());
141+
}
142+
143+
// IPv6: binding to ::ffff:127.0.0.1 (IPv4-mapped loopback) must succeed.
144+
// Dual-stack sockets can bind to IPv4 addresses in mapped form; rejecting this
145+
// would break applications that use IPv6 sockets for both IPv4 and IPv6.
146+
TEST(RestrictBindLoopback, IPv6_IPv4MappedLoopback_Allowed) {
147+
SKIP_IF(!RestrictBindToLoopbackEnabled());
148+
149+
FileDescriptor sock =
150+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
151+
struct sockaddr_in6 addr = {};
152+
addr.sin6_family = AF_INET6;
153+
addr.sin6_port = 0;
154+
// ::ffff:127.0.0.1
155+
inet_pton(AF_INET6, "::ffff:127.0.0.1", &addr.sin6_addr);
156+
EXPECT_THAT(
157+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
158+
sizeof(addr)),
159+
SyscallSucceeds());
160+
}
161+
162+
// IPv6: binding to ::ffff:0.0.0.0 (IPv4-mapped INADDR_ANY) must fail.
163+
TEST(RestrictBindLoopback, IPv6_IPv4MappedAny_Rejected) {
164+
SKIP_IF(!RestrictBindToLoopbackEnabled());
165+
166+
FileDescriptor sock =
167+
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_STREAM, 0));
168+
struct sockaddr_in6 addr = {};
169+
addr.sin6_family = AF_INET6;
170+
// ::ffff:0.0.0.0
171+
inet_pton(AF_INET6, "::ffff:0.0.0.0", &addr.sin6_addr);
172+
EXPECT_THAT(
173+
bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&addr),
174+
sizeof(addr)),
175+
SyscallFailsWithErrno(EACCES));
176+
}
177+
178+
} // namespace
179+
180+
} // namespace testing
181+
} // namespace gvisor

0 commit comments

Comments
 (0)