Skip to content

Commit 177ec9e

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 (::). IPv4 loopback is defined as 127.x.x.x (RFC 5735 loopback block). IPv6 loopback accepts ::1 and ::ffff:127.x.x.x — the IPv4-mapped form is permitted so that dual-stack sockets can bind to the IPv4 loopback without being rejected. ::ffff:0.0.0.0 (IPv4-mapped INADDR_ANY) is rejected. 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 177ec9e

8 files changed

Lines changed: 287 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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,26 @@ func Accept(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr,
346346
return n, nil, err
347347
}
348348

349+
// isIPv6Loopback reports whether addr is an IPv6 address that resolves to a
350+
// loopback interface: ::1 (IPv6 loopback) or ::ffff:127.x.x.x (IPv4-mapped
351+
// IPv4 loopback). The latter is necessary because dual-stack sockets accept
352+
// both IPv4 and IPv6 addresses, so an application may legitimately bind to the
353+
// IPv4 loopback through an IPv6 socket using the mapped form.
354+
func isIPv6Loopback(addr linux.Inet6Addr) bool {
355+
// ::1
356+
if addr == (linux.Inet6Addr{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) {
357+
return true
358+
}
359+
// ::ffff:127.x.x.x — IPv4-mapped prefix is 10 zero bytes followed by 0xff 0xff.
360+
if addr[0] == 0 && addr[1] == 0 && addr[2] == 0 && addr[3] == 0 &&
361+
addr[4] == 0 && addr[5] == 0 && addr[6] == 0 && addr[7] == 0 &&
362+
addr[8] == 0 && addr[9] == 0 && addr[10] == 0xff && addr[11] == 0xff &&
363+
addr[12] == 127 {
364+
return true
365+
}
366+
return false
367+
}
368+
349369
// Bind implements the linux syscall bind(2).
350370
func Bind(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) {
351371
fd := args[0].Int()
@@ -371,6 +391,33 @@ func Bind(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *
371391
return 0, nil, err
372392
}
373393

394+
// Enforce restrict-bind-to-loopback policy: reject any bind to a
395+
// non-loopback address (including INADDR_ANY / IN6ADDR_ANY) so that
396+
// sandboxed processes cannot inadvertently expose services on external
397+
// interfaces.
398+
if t.Kernel().RestrictBindToLoopback && len(a) >= 2 {
399+
family := hostarch.ByteOrder.Uint16(a)
400+
switch int(family) {
401+
case linux.AF_INET:
402+
if len(a) >= linux.SockAddrInetSize {
403+
var sa linux.SockAddrInet
404+
sa.UnmarshalUnsafe(a)
405+
// Allow only 127.x.x.x (RFC 5735 loopback block).
406+
if sa.Addr[0] != 127 {
407+
return 0, nil, linuxerr.EACCES
408+
}
409+
}
410+
case linux.AF_INET6:
411+
if len(a) >= linux.SockAddrInet6Size {
412+
var sa linux.SockAddrInet6
413+
sa.UnmarshalUnsafe(a)
414+
if !isIPv6Loopback(sa.Addr) {
415+
return 0, nil, linuxerr.EACCES
416+
}
417+
}
418+
}
419+
}
420+
374421
return 0, nil, s.Bind(t, a).ToError()
375422
}
376423

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)