mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-28 22:19:12 -04:00
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>
124 lines
3.6 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|