Files
Tamir DubersteinandHanzo Dev da02c90fb2 checklocks: synchronize proc network sysctls
Read SACK and ephemeral port ranges from the owning stack instead of
per-file caches. Separate proc mounts can cache different values, and a
rejected SACK write can change its cache. The saved stack already
preserves these settings.

Keep the last successfully written forwarding value, since individual
interfaces may have different forwarding settings. Guard it with a mutex
and publish updates only after the stack setter succeeds, leaving failed
writes out of subsequent readback.

Also make the existing TCP buffer-size helper contracts checkable. Their
mutex still serializes requests through the same byte source.

Cover shared readback, rejected SACK and forwarding writes, and
concurrent forwarding readback in the existing proc test target.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 13:58:53 -04:00

376 lines
10 KiB
Go

// Copyright 2019 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.
package proc
import (
"bytes"
"reflect"
"slices"
"strings"
"sync"
"testing"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/contexttest"
"gvisor.dev/gvisor/pkg/sentry/inet"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/usermem"
)
func newIPv6TestStack() *inet.TestStack {
s := inet.NewTestStack()
s.SupportsIPv6Flag = true
return s
}
func TestIfinet6NoAddresses(t *testing.T) {
n := &ifinet6{stack: newIPv6TestStack()}
var buf bytes.Buffer
n.Generate(contexttest.Context(t), &buf)
if buf.Len() > 0 {
t.Errorf("n.Generate() generated = %v, want = %v", buf.Bytes(), []byte{})
}
}
func TestIfinet6(t *testing.T) {
s := newIPv6TestStack()
s.InterfacesMap[1] = inet.Interface{Name: "eth0"}
s.InterfaceAddrsMap[1] = []inet.InterfaceAddr{
{
Family: linux.AF_INET6,
PrefixLen: 128,
Addr: []byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"),
},
}
s.InterfacesMap[2] = inet.Interface{Name: "eth1"}
s.InterfaceAddrsMap[2] = []inet.InterfaceAddr{
{
Family: linux.AF_INET6,
PrefixLen: 128,
Addr: []byte("\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"),
},
}
want := map[string]struct{}{
"000102030405060708090a0b0c0d0e0f 01 80 00 00 eth0\n": {},
"101112131415161718191a1b1c1d1e1f 02 80 00 00 eth1\n": {},
}
n := &ifinet6{stack: s}
contents := n.contents()
if len(contents) != len(want) {
t.Errorf("Got len(n.contents()) = %d, want = %d", len(contents), len(want))
}
got := map[string]struct{}{}
for _, l := range contents {
got[l] = struct{}{}
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Got n.contents() = %v, want = %v", got, want)
}
}
func TestNetStatDataRowsHaveMatchingFields(t *testing.T) {
n := &netStatData{}
var buf bytes.Buffer
if err := n.Generate(contexttest.Context(t), &buf); err != nil {
t.Fatalf("n.Generate() failed: %v", err)
}
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
if len(lines)%2 != 0 {
t.Fatalf("got %d lines, want name/value line pairs: %q", len(lines), buf.String())
}
for i := 0; i < len(lines); i += 2 {
names := strings.Fields(lines[i])
values := strings.Fields(lines[i+1])
if len(names) != len(values) {
t.Errorf("line %d has %d names, line %d has %d values: names=%q values=%q", i, len(names), i+1, len(values), lines[i], lines[i+1])
}
if names[0] != values[0] {
t.Errorf("line %d prefix = %q, line %d prefix = %q, want matching prefixes", i, names[0], i+1, values[0])
}
for _, value := range values[1:] {
if value != "0" {
t.Errorf("got netstat value %q, want 0", value)
}
}
}
}
type readOnlySysctlTestStack struct {
*inet.TestStack
}
func (*readOnlySysctlTestStack) SetTCPSACKEnabled(bool) error {
return linuxerr.EACCES
}
func (*readOnlySysctlTestStack) SetForwarding(tcpip.NetworkProtocolNumber, bool) error {
return linuxerr.EACCES
}
func TestTCPSACKReadback(t *testing.T) {
for _, tc := range []struct {
name string
writeErr error
want string
}{
{name: "shared stack", want: "1\n"},
{name: "rejected write", writeErr: linuxerr.EACCES, want: "0\n"},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := contexttest.Context(t)
s := inet.NewTestStack()
s.TCPSACKFlag = false
reader := &tcpSackData{stack: s}
writer := &tcpSackData{stack: s}
if tc.writeErr != nil {
writer = &tcpSackData{stack: &readOnlySysctlTestStack{TestStack: s}}
reader = writer
}
var buf bytes.Buffer
if err := reader.Generate(ctx, &buf); err != nil {
t.Fatal(err)
}
const value = "1\n"
if n, err := writer.Write(ctx, nil, usermem.BytesIOSequence([]byte(value)), 0); n != int64(len(value)) || err != tc.writeErr {
t.Fatalf("Write() = (%d, %v), want (%d, %v)", n, err, len(value), tc.writeErr)
}
buf.Reset()
if err := reader.Generate(ctx, &buf); err != nil {
t.Fatal(err)
}
if got := buf.String(); got != tc.want {
t.Errorf("Generate() = %q, want %q", got, tc.want)
}
})
}
}
// TestConfigureIPForwarding tests the implementation of
// /proc/sys/net/ipv4/ip_forward.
func TestConfigureIPForwarding(t *testing.T) {
ctx := context.Background()
s := inet.NewTestStack()
var cases = []struct {
comment string
initial bool
str string
final bool
}{
{
comment: `Forwarding is disabled; write 1 and enable forwarding`,
initial: false,
str: "1",
final: true,
},
{
comment: `Forwarding is disabled; write 0 and disable forwarding`,
initial: false,
str: "0",
final: false,
},
{
comment: `Forwarding is enabled; write 1 and enable forwarding`,
initial: true,
str: "1",
final: true,
},
{
comment: `Forwarding is enabled; write 0 and disable forwarding`,
initial: true,
str: "0",
final: false,
},
{
comment: `Forwarding is disabled; write 2404 and enable forwarding`,
initial: false,
str: "2404",
final: true,
},
{
comment: `Forwarding is enabled; write 2404 and enable forwarding`,
initial: true,
str: "2404",
final: true,
},
}
for _, c := range cases {
t.Run(c.comment, func(t *testing.T) {
s.IPForwarding = c.initial
file := &ipForwarding{stack: s, enabled: c.initial}
// Write the values.
src := usermem.BytesIOSequence([]byte(c.str))
if n, err := file.Write(ctx, nil, src, 0); n != int64(len(c.str)) || err != nil {
t.Errorf("file.Write(ctx, nil, %q, 0) = (%d, %v); want (%d, nil)", c.str, n, err, len(c.str))
}
// Read the values from the stack and check them.
if got, want := s.IPForwarding, c.final; got != want {
t.Errorf("s.IPForwarding incorrect; got: %v, want: %v", got, want)
}
})
}
t.Run("rejected write", func(t *testing.T) {
ctx := contexttest.Context(t)
ipf := &ipForwarding{stack: &readOnlySysctlTestStack{TestStack: inet.NewTestStack()}}
if n, err := ipf.Write(ctx, nil, usermem.BytesIOSequence([]byte("1\n")), 0); n != 0 || err != linuxerr.EACCES {
t.Fatalf("Write() = (%d, %v), want (0, %v)", n, err, linuxerr.EACCES)
}
var buf bytes.Buffer
if err := ipf.Generate(ctx, &buf); err != nil {
t.Fatal(err)
}
if got := buf.String(); got != "0\n" {
t.Errorf("Generate() = %q, want %q", got, "0\n")
}
})
t.Run("concurrent readback", func(t *testing.T) {
ctx := contexttest.Context(t)
ipf := &ipForwarding{stack: inet.NewTestStack()}
// Leave the read and write unordered so race builds check the shared flag.
start := make(chan struct{})
var wg sync.WaitGroup
wg.Go(func() {
<-start
const value = "1\n"
if n, err := ipf.Write(ctx, nil, usermem.BytesIOSequence([]byte(value)), 0); n != int64(len(value)) || err != nil {
t.Errorf("Write() = (%d, %v), want (%d, nil)", n, err, len(value))
}
})
wg.Go(func() {
<-start
var buf bytes.Buffer
if err := ipf.Generate(ctx, &buf); err != nil {
t.Errorf("Generate(): %v", err)
} else if got := buf.String(); got != "0\n" && got != "1\n" {
t.Errorf("Generate() = %q, want 0 or 1", got)
}
})
close(start)
wg.Wait()
var buf bytes.Buffer
if err := ipf.Generate(ctx, &buf); err != nil {
t.Fatal(err)
}
if got := buf.String(); got != "1\n" {
t.Errorf("Generate() = %q, want %q", got, "1\n")
}
})
}
type portRangeTestStack struct {
*inet.TestStack
start, end uint16
}
func (s *portRangeTestStack) PortRange() (uint16, uint16) {
return s.start, s.end
}
func (s *portRangeTestStack) SetPortRange(start, end uint16) error {
s.start, s.end = start, end
return nil
}
func TestPortRangeSharedStack(t *testing.T) {
ctx := contexttest.Context(t)
s := &portRangeTestStack{TestStack: inet.NewTestStack(), start: 32768, end: 60999}
reader := &portRange{stack: s}
writer := &portRange{stack: s}
var buf bytes.Buffer
if err := reader.Generate(ctx, &buf); err != nil {
t.Fatal(err)
}
const updated = "40000 50000\n"
if n, err := writer.Write(ctx, nil, usermem.BytesIOSequence([]byte(updated)), 0); n != int64(len(updated)) || err != nil {
t.Fatalf("Write() = (%d, %v), want (%d, nil)", n, err, len(updated))
}
buf.Reset()
if err := reader.Generate(ctx, &buf); err != nil {
t.Fatal(err)
}
if got := buf.String(); got != updated {
t.Errorf("Generate() = %q, want %q", got, updated)
}
}
func TestParseInt32Vec(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
src []byte
buf []int32
expectedBuf []int32
expectedBytes int64
}{
{
name: "EmptyInput",
src: []byte(""),
buf: []int32{},
expectedBuf: []int32{},
expectedBytes: 0,
},
{
name: "SingleNum",
src: []byte("123"),
buf: []int32{0},
expectedBuf: []int32{123},
expectedBytes: 3,
},
{
name: "MultipleNum",
src: []byte("123 456"),
buf: []int32{0, 0},
expectedBuf: []int32{123, 456},
expectedBytes: 7,
},
{
name: "FirstNNum",
src: []byte("123 456"),
buf: []int32{0, 0, 789},
expectedBuf: []int32{123, 456, 789},
expectedBytes: 7,
},
{
name: "ParsePartially",
src: []byte("123 a"),
buf: []int32{0},
expectedBuf: []int32{123},
expectedBytes: 4,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
originalBuf := append([]int32(nil), test.buf...)
n, err := ParseInt32Vec(ctx, usermem.BytesIOSequence(test.src), test.buf)
if !slices.Equal(test.buf, test.expectedBuf) || n != test.expectedBytes {
t.Errorf("ParseInt32Vec(ctx, %v, %v) returns %v, %v, result buffer is %v; expected: %v, %v", test.src, originalBuf, n, err, test.buf, test.expectedBuf, test.expectedBytes)
}
})
}
}