mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
Update the Go module path, imports, install scripts, docs, workflows, packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm scope (already @twigpine/zero on main, with its @gitlawb/zero migration notes), LICENSE, action author, the OpenGateway provider and historical CHANGELOG links are unchanged. The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what AIMLAPI sees for attribution, so changing it needs a maintainer's yes. docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because every tagged release so far declares that module path. The Homebrew formula comment names the Twigpine/homebrew-tap tap. Co-authored-by: Claude Opus 5.5 <[email protected]>
203 lines
6.0 KiB
Go
203 lines
6.0 KiB
Go
package daemon
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/Twigpine/zero/internal/privatedir"
|
|
)
|
|
|
|
// maxUnixSocketPath bounds the socket path to the smallest platform sun_path
|
|
// limit so we surface a clear error instead of a cryptic bind failure. macOS
|
|
// allows 104 bytes incl. the NUL (so 103 chars); Linux allows 108. 103 is the
|
|
// safe cross-platform ceiling.
|
|
const maxUnixSocketPath = 103
|
|
|
|
func secureCustomRuntimeParents(paths Paths) error {
|
|
parents := []struct {
|
|
name string
|
|
path string
|
|
}{
|
|
{name: "socket", path: filepath.Dir(paths.Socket)},
|
|
{name: "lock", path: filepath.Dir(paths.Lock)},
|
|
{name: "status", path: filepath.Dir(paths.Status)},
|
|
}
|
|
seen := make(map[string]struct{}, len(parents))
|
|
for _, parent := range parents {
|
|
absolute, err := filepath.Abs(parent.path)
|
|
if err != nil {
|
|
return fmt.Errorf("daemon: resolve %s directory: %w", parent.name, err)
|
|
}
|
|
if _, ok := seen[absolute]; ok {
|
|
continue
|
|
}
|
|
seen[absolute] = struct{}{}
|
|
if err := os.MkdirAll(absolute, 0o700); err != nil {
|
|
return fmt.Errorf("daemon: create %s directory: %w", parent.name, err)
|
|
}
|
|
root, err := os.OpenRoot(absolute)
|
|
if err != nil {
|
|
return fmt.Errorf("daemon: open custom %s directory: %w", parent.name, err)
|
|
}
|
|
validateErr := validateStatusRoot(root)
|
|
closeErr := root.Close()
|
|
if validateErr != nil {
|
|
return fmt.Errorf("daemon: validate custom %s directory: %w", parent.name, validateErr)
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("daemon: close custom %s directory: %w", parent.name, closeErr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// OpenRuntimeLog secures the default runtime root before acquiring the
|
|
// detached daemon's log descriptor. Custom endpoint layouts remain supported,
|
|
// but their existing parent permissions are caller-owned and left untouched.
|
|
func OpenRuntimeLog(paths Paths) (*os.File, string, error) {
|
|
logPath := filepath.Join(filepath.Dir(paths.Socket), "daemon.log")
|
|
root, isDefault, err := openDefaultRuntimeRoot(paths)
|
|
if err != nil {
|
|
return nil, logPath, err
|
|
}
|
|
if isDefault {
|
|
defer root.Close()
|
|
file, err := openRootAppendRegular(root, filepath.Base(logPath))
|
|
if err != nil {
|
|
return nil, logPath, fmt.Errorf("daemon: open runtime log: %w", err)
|
|
}
|
|
return file, logPath, nil
|
|
}
|
|
|
|
parent := filepath.Dir(logPath)
|
|
if err := os.MkdirAll(parent, 0o700); err != nil {
|
|
return nil, logPath, fmt.Errorf("daemon: create custom log directory: %w", err)
|
|
}
|
|
info, err := os.Lstat(logPath)
|
|
if err == nil && !info.Mode().IsRegular() {
|
|
return nil, logPath, fmt.Errorf("daemon: custom runtime log is not a regular file")
|
|
}
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return nil, logPath, fmt.Errorf("daemon: inspect custom runtime log: %w", err)
|
|
}
|
|
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
|
if err != nil {
|
|
return nil, logPath, fmt.Errorf("daemon: open custom runtime log: %w", err)
|
|
}
|
|
return file, logPath, nil
|
|
}
|
|
|
|
func openDefaultRuntimeRoot(paths Paths) (*os.Root, bool, error) {
|
|
defaults, err := DefaultPaths()
|
|
if err != nil {
|
|
// A caller-supplied layout remains usable even when the process has no
|
|
// resolvable home directory. Production default paths can only reach this
|
|
// helper after DefaultPaths has already succeeded.
|
|
return nil, false, nil
|
|
}
|
|
matches, err := samePaths(paths, defaults)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if !matches {
|
|
return nil, false, nil
|
|
}
|
|
root, err := privatedir.Open(filepath.Dir(defaults.Socket))
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("daemon: secure default runtime directory: %w", err)
|
|
}
|
|
return root, true, nil
|
|
}
|
|
|
|
func samePaths(left, right Paths) (bool, error) {
|
|
pairs := [][2]string{
|
|
{left.Socket, right.Socket},
|
|
{left.Lock, right.Lock},
|
|
{left.Status, right.Status},
|
|
}
|
|
for _, pair := range pairs {
|
|
leftAbs, err := filepath.Abs(pair[0])
|
|
if err != nil {
|
|
return false, fmt.Errorf("daemon: resolve runtime path: %w", err)
|
|
}
|
|
rightAbs, err := filepath.Abs(pair[1])
|
|
if err != nil {
|
|
return false, fmt.Errorf("daemon: resolve default runtime path: %w", err)
|
|
}
|
|
if filepath.Clean(leftAbs) != filepath.Clean(rightAbs) {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func runtimeRootStillNamesPath(root *os.Root, path string) error {
|
|
opened, err := root.Stat(".")
|
|
if err != nil {
|
|
return fmt.Errorf("inspect bound runtime directory: %w", err)
|
|
}
|
|
current, err := os.Lstat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect runtime directory entry: %w", err)
|
|
}
|
|
if current.Mode()&os.ModeSymlink != 0 || !os.SameFile(opened, current) {
|
|
return fmt.Errorf("daemon: default runtime directory changed after it was secured")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func captureBoundSocketRoot(path string) (*os.Root, string, error) {
|
|
root, err := os.OpenRoot(filepath.Dir(path))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("open bound socket directory: %w", err)
|
|
}
|
|
name := filepath.Base(path)
|
|
info, err := root.Lstat(name)
|
|
if err != nil {
|
|
_ = root.Close()
|
|
return nil, "", fmt.Errorf("inspect bound socket: %w", err)
|
|
}
|
|
if info.Mode()&os.ModeSocket == 0 {
|
|
_ = root.Close()
|
|
return nil, "", fmt.Errorf("bound socket path is not a socket")
|
|
}
|
|
return root, name, nil
|
|
}
|
|
|
|
func openRootAppendRegular(root *os.Root, name string) (*os.File, error) {
|
|
info, err := root.Lstat(name)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return root.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY|os.O_APPEND, 0o600)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return nil, fmt.Errorf("runtime log is not a regular file")
|
|
}
|
|
file, err := root.OpenFile(name, os.O_WRONLY|os.O_APPEND, 0o600)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opened, err := file.Stat()
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return nil, err
|
|
}
|
|
if !opened.Mode().IsRegular() {
|
|
_ = file.Close()
|
|
return nil, fmt.Errorf("runtime log changed while opening")
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
// checkSocketPathLength rejects an over-long unix socket path before bind.
|
|
func checkSocketPathLength(socketPath string) error {
|
|
if len(socketPath) > maxUnixSocketPath {
|
|
return fmt.Errorf("daemon: socket path too long (%d > %d): %s", len(socketPath), maxUnixSocketPath, socketPath)
|
|
}
|
|
return nil
|
|
}
|