204 lines
5.9 KiB
Go
204 lines
5.9 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentacle
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
type launchReply struct {
|
|
PID int
|
|
Errno uint32
|
|
}
|
|
|
|
// launch passes exactly four descriptors: stdin, stdout, stderr and the already
|
|
// limited cgroup. The service performs clone3 inside its delegated tree. No
|
|
// shell, environment update, or settings operation is exposed by this protocol.
|
|
func (delegation *Delegation) launch(ctx context.Context, spec Spec, files ...*os.File) (int, <-chan struct{}, func(), error) {
|
|
connection, err := net.DialUnix("unixpacket", nil, &net.UnixAddr{Name: filepath.Join(delegation.directory, "launch"), Net: "unixpacket"})
|
|
if err != nil {
|
|
return 0, nil, nil, err
|
|
}
|
|
keep := false
|
|
defer func() {
|
|
if !keep {
|
|
connection.Close()
|
|
}
|
|
}()
|
|
connection.SetDeadline(time.Now().Add(3 * time.Second))
|
|
stopCancel := context.AfterFunc(ctx, func() { connection.Close() })
|
|
defer func() {
|
|
if !keep {
|
|
stopCancel()
|
|
}
|
|
}()
|
|
data, err := json.Marshal(spec)
|
|
if err != nil || len(data) > MaxFrameBytes || len(files) != 4 {
|
|
return 0, nil, nil, ErrLimit
|
|
}
|
|
descriptors := make([]int, len(files))
|
|
for index, file := range files {
|
|
descriptors[index] = int(file.Fd())
|
|
}
|
|
if _, _, err := connection.WriteMsgUnix(data, syscall.UnixRights(descriptors...), nil); err != nil {
|
|
return 0, nil, nil, err
|
|
}
|
|
buffer := make([]byte, 256)
|
|
count, err := connection.Read(buffer)
|
|
if err != nil {
|
|
return 0, nil, nil, err
|
|
}
|
|
var reply launchReply
|
|
if err := json.Unmarshal(buffer[:count], &reply); err != nil {
|
|
return 0, nil, nil, ErrProtocol
|
|
}
|
|
if reply.Errno != 0 {
|
|
return 0, nil, nil, syscall.Errno(reply.Errno)
|
|
}
|
|
if reply.PID <= 0 {
|
|
return 0, nil, nil, ErrProtocol
|
|
}
|
|
connection.SetDeadline(time.Time{})
|
|
done := make(chan struct{})
|
|
go func() { defer stopCancel(); var end [1]byte; connection.Read(end[:]); connection.Close(); close(done) }()
|
|
keep = true
|
|
return reply.PID, done, func() { connection.Close() }, nil
|
|
}
|
|
|
|
func serve_launches(input io.Reader, output io.Writer, socketPath, parent string) error {
|
|
listener, err := net.ListenUnix("unixpacket", &net.UnixAddr{Name: socketPath, Net: "unixpacket"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer listener.Close()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
var workers sync.WaitGroup
|
|
capacity := make(chan struct{}, DefaultLimits().Processes)
|
|
accepted := make(chan struct{})
|
|
go func() {
|
|
defer close(accepted)
|
|
for {
|
|
connection, err := listener.AcceptUnix()
|
|
if err != nil {
|
|
return
|
|
}
|
|
select {
|
|
case capacity <- struct{}{}:
|
|
workers.Add(1)
|
|
go func() { defer workers.Done(); defer func() { <-capacity }(); serve_launch(ctx, connection, parent) }()
|
|
default:
|
|
connection.Close()
|
|
}
|
|
}
|
|
}()
|
|
defer func() { cancel(); listener.Close(); <-accepted; workers.Wait() }()
|
|
if _, err := fmt.Fprintln(output, parent); err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(io.Discard, input)
|
|
return err
|
|
}
|
|
|
|
func serve_launch(ctx context.Context, connection *net.UnixConn, parent string) {
|
|
defer connection.Close()
|
|
stop := context.AfterFunc(ctx, func() { connection.Close() })
|
|
defer stop()
|
|
connection.SetDeadline(time.Now().Add(3 * time.Second))
|
|
data, ancillary := make([]byte, MaxFrameBytes), make([]byte, syscall.CmsgSpace(4*4))
|
|
count, rightsCount, flags, _, err := connection.ReadMsgUnix(data, ancillary)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Always close received descriptors, even when framing or validation fails.
|
|
var files []*os.File
|
|
defer func() {
|
|
for _, file := range files {
|
|
file.Close()
|
|
}
|
|
}()
|
|
messages, err := syscall.ParseSocketControlMessage(ancillary[:rightsCount])
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, message := range messages {
|
|
descriptors, err := syscall.ParseUnixRights(&message)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, descriptor := range descriptors {
|
|
syscall.CloseOnExec(descriptor)
|
|
files = append(files, os.NewFile(uintptr(descriptor), "tentacle-launch"))
|
|
}
|
|
}
|
|
if flags&(syscall.MSG_TRUNC|syscall.MSG_CTRUNC) != 0 || len(files) != 4 {
|
|
return
|
|
}
|
|
var spec Spec
|
|
if decode_strict(data[:count], &spec) != nil || validate_spec(spec) != nil {
|
|
return
|
|
}
|
|
var stat syscall.Statfs_t
|
|
if syscall.Fstatfs(int(files[3].Fd()), &stat) != nil || stat.Type != 0x63677270 {
|
|
return
|
|
}
|
|
groupPath, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", files[3].Fd()))
|
|
if err != nil {
|
|
return
|
|
}
|
|
relative, err := filepath.Rel(parent, groupPath)
|
|
parts := strings.Split(relative, string(filepath.Separator))
|
|
if err != nil || len(parts) != 2 || !strings.HasPrefix(parts[0], "cthulhu-tentacles-") {
|
|
return
|
|
}
|
|
for _, identity := range []string{strings.TrimPrefix(parts[0], "cthulhu-tentacles-"), parts[1]} {
|
|
if decoded, err := hex.DecodeString(identity); err != nil || len(decoded) != 16 {
|
|
return
|
|
}
|
|
}
|
|
command := exec.Command(spec.Program[0], spec.Program[1:]...)
|
|
command.Dir = spec.Directory
|
|
command.Stdin, command.Stdout, command.Stderr = files[0], files[1], files[2]
|
|
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, UseCgroupFD: true, CgroupFD: int(files[3].Fd())}
|
|
if err := command.Start(); err != nil {
|
|
var errno syscall.Errno
|
|
if !errors.As(err, &errno) {
|
|
errno = syscall.EIO
|
|
}
|
|
reply, _ := json.Marshal(launchReply{Errno: uint32(errno)})
|
|
connection.Write(reply)
|
|
return
|
|
}
|
|
for _, file := range files {
|
|
file.Close()
|
|
}
|
|
files = nil
|
|
// A closed launch connection kills the direct child. The reader's cgroup
|
|
// cleanup or systemd's guardian-exit cleanup covers every descendant.
|
|
disconnected := make(chan struct{})
|
|
go func() { defer close(disconnected); var end [1]byte; connection.Read(end[:]); command.Process.Kill() }()
|
|
reply, _ := json.Marshal(launchReply{PID: command.Process.Pid})
|
|
if _, err := connection.Write(reply); err != nil {
|
|
command.Process.Kill()
|
|
}
|
|
connection.SetDeadline(time.Time{})
|
|
_ = command.Wait()
|
|
connection.Write([]byte{0})
|
|
connection.Close()
|
|
<-disconnected
|
|
}
|