Files
ctrld/cmd/cli/log_send_darwin.go
T

194 lines
6.2 KiB
Go

package cli
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"syscall"
"time"
"unsafe"
"github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/controld"
"golang.org/x/sys/unix"
)
// /private/var/run is group-writable on macOS and fails the trust walk.
const logSendSocketDir = "/private/var/ctrld-log-send"
const logSendSocketPath = logSendSocketDir + "/send.sock"
const delegatedJournalBytes int64 = 6 << 20
func logSendPeerUID(fd int) (uint32, error) {
cred, err := unix.GetsockoptXucred(fd, unix.SOL_LOCAL, unix.LOCAL_PEERCRED)
if err != nil {
return 0, err
}
// XNU bsd/sys/ucred.h defines XUCRED_VERSION as 0; this version of
// x/sys/unix exposes Xucred but does not export that constant.
if cred.Version != 0 {
return 0, errLogSendTrust
}
return cred.Uid, nil
}
// A POSIX mode alone cannot exclude a macOS ACL grant. Conservatively refuse
// extended ACLs (even deny-only ACLs), and fail closed if the kernel cannot report
// them. fgetattrlist returns a length and an attrreference; absent ACL => length 0.
// See XNU vfs_attrlist.c, ATTR_CMN_EXTENDED_SECURITY packing. Using the open fd
// avoids a pathname race and does not need cgo or a privileged helper process.
func logSendCheckACL(fd int) error {
attrs := unix.Attrlist{Bitmapcount: 5, Commonattr: unix.ATTR_CMN_EXTENDED_SECURITY}
var buf [12]byte
//lint:ignore SA1019 x/sys has no fgetattrlist libSystem wrapper; preserve descriptor-relative ACL checks in CGO_ENABLED=0 builds.
_, _, errno := syscall.Syscall6(unix.SYS_FGETATTRLIST, uintptr(fd), uintptr(unsafe.Pointer(&attrs)), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0)
if errno != 0 {
return errno
}
if binary.LittleEndian.Uint32(buf[:4]) != 12 || binary.LittleEndian.Uint32(buf[8:]) != 0 {
return errLogSendTrust
}
return nil
}
func delegatedLogSendCLI() bool { return os.Geteuid() != 0 }
func runDelegatedLogSend(ctx context.Context, full bool) error {
err := requestDelegatedLogSend(ctx, full, func(ctx context.Context) (net.Conn, error) {
return dialLogSend(ctx, trustedLogTree{root: "/", uid: 0}, logSendSocketPath)
})
if err != nil {
return fmt.Errorf("failed to send logs: %w", err)
}
mainLog.Load().Notice().Msg("Runtime logs sent successfully")
return nil
}
func (p *prog) startLogSendServer() {
p.logSendMu.Lock()
defer p.logSendMu.Unlock()
if p.logSendStopped || p.logSend != nil {
return
}
p.mu.Lock()
enabled := p.cfg.Service.AllowUnprivilegedLogSend
p.mu.Unlock()
if !enabled {
return
}
s, err := p.newDelegatedLogSendServer()
if err != nil {
p.Error().Err(err).Msg("Unprivileged log send disabled: unsafe or unavailable configuration, logs or socket; correct as administrator and restart")
return
}
p.logSend = s
ctrld.Journal(p.Info()).Msg("Unprivileged log send enabled for all local users until service stop")
}
func (p *prog) newDelegatedLogSendServer() (*logSendServer, error) {
if os.Geteuid() != 0 || cdUID == "" || configBase64 != "" {
return nil, errLogSendTrust
}
tree := trustedLogTree{root: "/", uid: 0}
collector := delegatedLogCollector{tree: tree, config: canonicalLogSendPath(v.ConfigFileUsed())}
// Capture source paths and budgets once. A reload cannot redirect this socket
// to new files, identities, or destinations, nor enable an initially off socket.
var files []*rotatingFile
if p.needInternalLogging() {
lw, jw := p.internalWriters()
if lw == nil || jw == nil {
return nil, errLogSendTrust
}
files = []*rotatingFile{lw.rotating(), jw.rotating()}
} else {
files = []*rotatingFile{logPathFile.Load()}
}
for i, rf := range files {
if rf == nil {
return nil, errLogSendTrust
}
rf.mu.Lock()
source := delegatedLogSource{path: canonicalLogSendPath(rf.path), backups: rf.budget.backups, budget: delegatedDebugBytes}
rf.mu.Unlock()
if i == 1 {
source.budget = delegatedJournalBytes
}
collector.sources = append(collector.sources, source)
}
listener, err := collector.listen(logSendSocketDir)
if err != nil {
return nil, err
}
uid, dev := cdUID, cdDev
s := &logSendServer{gate: &p.logUpload, collect: collector.collect,
upload: func(ctx context.Context, body io.ReadCloser) error {
return controld.SendLogs(ctrld.LoggerCtx(ctx, p.logger.Load()), &controld.LogsRequest{UID: uid, Data: body}, dev)
},
audit: func(uid int64, code int) {
ctrld.Journal(p.Info()).Int64("caller_uid", uid).Int("status", code).Msg("Unprivileged log send outcome")
},
}
s.serve(listener, logSendPeer)
return s, nil
}
func (c delegatedLogCollector) listen(dir string) (net.Listener, error) {
// Validate before exposing any capability; repeat the same checks per upload.
if err := c.validate(context.Background()); err != nil {
return nil, err
}
return listenDelegatedLogSend(c.tree, dir)
}
func listenDelegatedLogSend(tree trustedLogTree, dir string) (net.Listener, error) {
parent, err := tree.open(filepath.Dir(dir), true)
if err != nil {
return nil, err
}
defer parent.Close()
if err := unix.Mkdirat(int(parent.Fd()), filepath.Base(dir), 0755); err != nil && !errors.Is(err, unix.EEXIST) {
return nil, err
}
protected, err := tree.open(dir, true)
if err != nil {
return nil, err
}
protected.Close()
path := filepath.Join(dir, "send.sock")
if st, err := os.Lstat(path); err == nil {
stat, ok := st.Sys().(*syscall.Stat_t)
if !ok || stat.Uid != tree.uid || st.Mode()&os.ModeSocket == 0 {
return nil, errLogSendTrust
}
c, err := net.DialTimeout("unix", path, time.Second)
if err == nil {
c.Close()
return nil, errors.New("log-send socket already active")
}
// Only an unequivocally stale endpoint may be removed, not timeout/permission errors.
if !errors.Is(err, syscall.ECONNREFUSED) {
return nil, err
}
if err := os.Remove(path); err != nil {
return nil, err
}
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
if err != nil {
return nil, err
}
// No handler is serving until chmod succeeds. Parent ownership prevents
// replacement. The administrative control socket remains 0600.
if err := os.Chmod(path, 0666); err != nil {
l.Close()
return nil, err
}
l.SetUnlinkOnClose(true)
return l, nil
}