Files
zip/files_unix.go
zeekayandHanzo Dev 02f2e3da97 Carry file descriptors over unix connections
A descriptor is not data: its number is an index into a table the peer does not
share, so a byte stream cannot hand over a pipe, a console, or an already-open
file. SCM_RIGHTS is the kernel doing that translation, and it rides the same
unix connection peerOf already reads SO_PEERCRED from.

Needed to carry a sandbox control plane. gVisor's urpc passes Files on 20 of
its 22 call sites — exec's stdio, checkpoint restore, the memory file — so a
transport without this cannot replace it at all.

Refuses rather than half-delivering: an empty body would let the kernel discard
a control-only message and lose the descriptors while the call reports success,
and a non-unix connection would deliver the bytes without the files.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 23:57:32 -07:00

151 lines
4.9 KiB
Go

//go:build linux || darwin
package zip
import (
"errors"
"fmt"
"net"
"os"
"syscall"
)
// Passing open files, which is the one thing a byte stream cannot express.
//
// A descriptor is not data. Sending its NUMBER means nothing across a process
// boundary — the integer is an index into a table the peer does not share — so
// a socket that carries only bytes cannot hand over a pipe, a console or an
// already-open file. SCM_RIGHTS is the kernel doing the translation: it
// duplicates the descriptor into the receiver's table and reports the number it
// landed on there.
//
// This is why a sandbox supervisor needs it. The gofer's connection, a
// process's stdio and a restored memory file all exist before the call that
// hands them over, and none of them can be reopened by name on the other side.
//
// It rides the same unix connection as everything else, so nothing about
// addressing or dialling changes.
// ErrNoFilePassing reports a connection that cannot carry descriptors. It is a
// distinct error rather than a silent zero because dropping the files while
// delivering the bytes is the worst outcome: the receiver gets a message that
// says a console was attached and no console.
var ErrNoFilePassing = errors.New("zip: file passing needs a unix connection")
// maxFiles bounds one message. SCM_RIGHTS is limited by the receiver's own
// descriptor table and by SCM_MAX_FD (253 on Linux); a request for more is
// refused here, where the number is known, rather than by a sendmsg errno that
// says only EINVAL.
const maxFiles = 253
// SendFiles writes b and hands over files on the same message.
//
// The byte is not optional. A message carrying only control data may be
// discarded before the receiver ever sees it, so at least one real byte must
// travel with the descriptors or the transfer is silently lost — which is why
// this refuses an empty b rather than sending something that usually works.
func SendFiles(c net.Conn, b []byte, files []*os.File) error {
if len(b) == 0 {
return errors.New("zip: SendFiles needs at least one byte alongside the files")
}
if len(files) > maxFiles {
return fmt.Errorf("zip: %d files exceeds the %d a message may carry", len(files), maxFiles)
}
uc, ok := c.(*net.UnixConn)
if !ok {
return ErrNoFilePassing
}
fds := make([]int, len(files))
for i, f := range files {
if f == nil {
return fmt.Errorf("zip: file %d is nil", i)
}
fds[i] = int(f.Fd())
}
// Hold every *os.File alive across the syscall. Without this the finalizer
// may close a descriptor after Fd() read it and before the kernel copies
// it, and the peer receives a number pointing at whatever was opened next.
defer runtimeKeepFiles(files)
var rights []byte
if len(fds) > 0 {
rights = syscall.UnixRights(fds...)
}
raw, err := uc.SyscallConn()
if err != nil {
return err
}
var sendErr error
if err := raw.Control(func(fd uintptr) {
_, sendErr = syscall.SendmsgN(int(fd), b, rights, nil, 0)
}); err != nil {
return err
}
return sendErr
}
// RecvFiles reads one message into b and returns the descriptors that came
// with it, already wrapped as *os.File so they are closed by the usual means.
//
// Every received descriptor is closed if the call fails afterwards. A leaked
// descriptor from a rejected message is invisible until the process runs out,
// and then it is attributed to whatever asked for the next one.
func RecvFiles(c net.Conn, b []byte) (int, []*os.File, error) {
uc, ok := c.(*net.UnixConn)
if !ok {
return 0, nil, ErrNoFilePassing
}
// Space for the control message. CmsgSpace accounts for the header and the
// alignment the kernel requires; sizing this by hand is how a truncated
// SCM_RIGHTS silently drops the last descriptor.
oob := make([]byte, syscall.CmsgSpace(maxFiles*4))
raw, err := uc.SyscallConn()
if err != nil {
return 0, nil, err
}
var n, oobn int
var recvErr error
if err := raw.Read(func(fd uintptr) bool {
n, oobn, _, _, recvErr = syscall.Recvmsg(int(fd), b, oob, recvFlags)
// Report not-ready so the runtime waits and calls again, rather than
// turning a would-block into a short read the caller reads as EOF.
if recvErr == syscall.EAGAIN || recvErr == syscall.EWOULDBLOCK {
return false
}
return true
}); err != nil {
return 0, nil, err
}
if recvErr != nil {
return 0, nil, recvErr
}
if oobn == 0 {
return n, nil, nil
}
msgs, err := syscall.ParseSocketControlMessage(oob[:oobn])
if err != nil {
return n, nil, err
}
var files []*os.File
closeAll := func() {
for _, f := range files {
_ = f.Close()
}
}
for _, m := range msgs {
if m.Header.Level != syscall.SOL_SOCKET || m.Header.Type != syscall.SCM_RIGHTS {
continue
}
fds, err := syscall.ParseUnixRights(&m)
if err != nil {
closeAll()
return n, nil, err
}
for _, fd := range fds {
setCloexec(fd)
files = append(files, os.NewFile(uintptr(fd), "zip-received-file"))
}
}
return n, files, nil
}