Files
tailscale/ssh/tailssh/monitor_rpc_linux_test.go
James Tucker 79d5abe043 ssh/tailssh: launch sessions via the monitor
Launch incubator sessions through the per-connection monitor, passing
stdio and the optional environment pipe via SCM_RIGHTS. tailscaled
retains the parent I/O descriptors. Keep direct launch when no
tailscaled binary is available, and on Plan 9.

Wait for dispatched exec replies even after session cancellation so
the session retains ownership of any child that starts. Publish exit
status through persistent completion state. Identify monitor children
by PID and exec request ID, and validate both when signaling. Remove
raw-PID signaling after monitor teardown.

Normalize incubator identity flags and reject alternate spellings
that could override them. The monitor RPC caller remains trusted to
supply the executable and other arguments; this is not a privilege
boundary against tailscaled.

Pass default TERM and locale values through the environment pipe,
preserving last-value-wins defaults and policy-forwarded precedence.
Use a separately owned close-on-exec PTY descriptor for resizing.

Add regression tests for launch cancellation, exit delivery, signal
identity, descriptor ownership, and environment handling.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 07:49:11 -07:00

124 lines
3.6 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package tailssh
import (
"testing"
"golang.org/x/sys/unix"
)
// Regression assertion for rejected SCM_RIGHTS ancillary data: any subset
// installed by the kernel must be closed, even when MSG_CTRUNC is set.
func TestMonitorRPCAncillaryTruncationDoesNotLeak(t *testing.T) {
pair, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
t.Fatal(err)
}
defer unix.Close(pair[0])
defer unix.Close(pair[1])
var original []int
defer func() { closeFds(original) }()
for range 4 {
fd, e := unix.Open("/dev/null", unix.O_RDONLY|unix.O_CLOEXEC, 0)
if e != nil {
t.Fatal(e)
}
original = append(original, fd)
}
if err := unix.Sendmsg(pair[0], []byte("x"), unix.UnixRights(original...), nil, 0); err != nil {
t.Fatal(err)
}
b, oob := make([]byte, 8), make([]byte, unix.CmsgSpace(4))
_, on, flags, _, err := unix.Recvmsg(pair[1], b, oob, unix.MSG_CMSG_CLOEXEC)
if err != nil {
t.Fatal(err)
}
if flags&unix.MSG_CTRUNC == 0 {
t.Fatalf("expected MSG_CTRUNC, flags=%x", flags)
}
_, recvErr := recvFdSets(oob[:on], flags)
if recvErr == nil {
t.Fatal("recvFdSets accepted truncated rights")
}
cmsgs, err := unix.ParseSocketControlMessage(oob[:on])
if err != nil {
t.Fatal(err)
}
var delivered []int
for _, cm := range cmsgs {
fds, e := unix.ParseUnixRights(&cm)
if e != nil {
t.Fatal(e)
}
delivered = append(delivered, fds...)
}
if len(delivered) == 0 {
t.Fatal("kernel delivered no rights, test cannot assert fd cleanup")
}
for _, fd := range delivered {
if _, e := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); e == nil {
unix.Close(fd) // Clean up only if recvFdSets failed to close it.
t.Errorf("recvFdSets leaked delivered fd %d after MSG_CTRUNC", fd)
} else if e != unix.EBADF {
t.Fatalf("fd %d status=%v", fd, e)
}
}
t.Logf("MSG_CTRUNC delivered %d rights; recvFdSets error=%v", len(delivered), recvErr)
}
func TestMonitorRPCUnexpectedControlMessageClosesRights(t *testing.T) {
for _, tc := range []struct {
name string
unexpectedFirst bool
}{
{"unexpected_after_rights", false},
{"unexpected_before_rights", true},
} {
t.Run(tc.name, func(t *testing.T) {
pair, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
t.Fatal(err)
}
defer unix.Close(pair[0])
defer unix.Close(pair[1])
sent := devNullFd(t)
if err := unix.Sendmsg(pair[0], []byte("x"), unix.UnixRights(sent), nil, 0); err != nil {
t.Fatal(err)
}
b, oob := make([]byte, 8), make([]byte, unix.CmsgSpace(4))
_, on, flags, _, err := unix.Recvmsg(pair[1], b, oob, unix.MSG_CMSG_CLOEXEC)
if err != nil {
t.Fatal(err)
}
cmsgs, err := unix.ParseSocketControlMessage(oob[:on])
if err != nil || len(cmsgs) != 1 {
t.Fatalf("received cmsgs=%v, err=%v", cmsgs, err)
}
delivered, err := unix.ParseUnixRights(&cmsgs[0])
if err != nil || len(delivered) != 1 {
t.Fatalf("delivered rights=%v, err=%v", delivered, err)
}
unexpected := unix.UnixCredentials(&unix.Ucred{})
var control []byte
if tc.unexpectedFirst {
control = append(unexpected, oob[:on]...)
} else {
control = append(append([]byte(nil), oob[:on]...), unexpected...)
}
if _, err := recvFdSets(control, flags); err == nil {
t.Fatal("unexpected control message accepted")
}
if _, err := unix.FcntlInt(uintptr(delivered[0]), unix.F_GETFD, 0); err != unix.EBADF {
if err == nil {
unix.Close(delivered[0])
}
t.Errorf("received right %d left open after unexpected control message: %v", delivered[0], err)
}
})
}
}