Skip to content
Closed
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 packages/envd/internal/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ var nfsOptions = strings.Join([]string{
"nfsvers=3", // nfs proxy is nfs version 3
"noacl", // no reason for acl in the sandbox

// The nfs proxy serves no nlockmgr, so NLM lock calls wedge forever (D state)
// under "hard". Locks are sandbox-local: no cross-sandbox lock coordination.
"nolock",

// disable caching so that pause/resume works correctly
"noac",
"lookupcache=none",
Expand Down
23 changes: 23 additions & 0 deletions packages/envd/internal/api/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,26 @@ func TestPostInit_UnauthorizedDoesNotUnfreeze(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, rec.Code)
assert.Empty(t, mgr.unfreezeAttempts, "unauthorized /init must not attempt unfreeze")
}

// The nfs proxy serves no NLM, so nfsOptions must keep locks local —
// otherwise "hard" retries the lock RPCs forever, wedging callers in D state.
func TestNFSOptionsResolveLocksLocally(t *testing.T) {
t.Parallel()

opts := make(map[string]string, 16)
for opt := range strings.SplitSeq(nfsOptions, ",") {
name, value, _ := strings.Cut(opt, "=")
opts[name] = value
}

_, hasNolock := opts["nolock"]
assert.True(t, hasNolock, "nfsOptions must contain nolock, got %q", nfsOptions)

_, hasLock := opts["lock"]
assert.False(t, hasLock, "nfsOptions must not contain lock, got %q", nfsOptions)

// Only local_lock=all keeps locks off the wire; "none" and "posix" still forward flock or fcntl to NLM.
if localLock, ok := opts["local_lock"]; ok {
assert.Equal(t, "all", localLock, "local_lock must not re-enable NLM, got %q", nfsOptions)
}
}
58 changes: 58 additions & 0 deletions packages/orchestrator/pkg/portmap/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,61 @@ func TestPortMapServer(t *testing.T) {
// verify results
assert.Equal(t, uint32(fakeNfsPort), uint32(output))
}

// nlockmgrProg is the RPC program number of the NFSv3 lock manager (NLM).
const nlockmgrProg = 100021

// envd mounts sandbox volumes with "nolock" because this proxy serves no NLM;
// the portmapper must keep answering 0 for nlockmgr.
func TestPortMapDoesNotAdvertiseNLM(t *testing.T) {
t.Parallel()

const fakeNfsPort = 19393

listenConfig := net.ListenConfig{}
lis, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() {
err := lis.Close()
assert.NoError(t, err)
})

s := NewPortMap(t.Context())
s.RegisterPort(t.Context(), fakeNfsPort)
go func() {
err := s.Serve(t.Context(), lis)
assert.NoError(t, err)
}()

a, err := net.ResolveTCPAddr("tcp", lis.Addr().String())
require.NoError(t, err)
conn, err := net.DialTCP("tcp", nil, a)
require.NoError(t, err)
t.Cleanup(func() {
err := conn.Close()
assert.NoError(t, err)
})
client := portmap.MakeClient(conn, portmap.PMAP_PROG, portmap.PMAP_VERS)

cred := portmap.Opaque_auth{Flavor: portmap.AUTH_NONE}
vfer := portmap.Opaque_auth{Flavor: portmap.AUTH_NONE}

getPort := func(t *testing.T, prog, vers uint32) uint32 {
t.Helper()

input := portmap.Mapping{Prog: prog, Vers: vers, Prot: portmap.IPPROTO_TCP}

var output portmap.Uint32
require.NoError(t, client.Call(portmap.PMAPPROC_GETPORT, cred, vfer, &input, &output))

return uint32(output)
}

// Control: nfs itself is advertised.
assert.Equal(t, uint32(fakeNfsPort), getPort(t, nfs.Nfs3Prog, nfs.Nfs3Vers))

for _, vers := range []uint32{1, 3, 4} {
assert.Zero(t, getPort(t, nlockmgrProg, vers),
"nlockmgr v%d must not be advertised", vers)
}
}
Loading