Attempt to make audio and speech discovery more robust.

This commit is contained in:
Storm Dragon committed 2026-11-03 13:21:50 -05:00
1 parent a27f274099
commit dc945ff392
21 files changed
+1314 -60

No files matched your search

+65 -4
View File
@@ -11,6 +11,8 @@ import (
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
@@ -102,10 +104,6 @@ func spawn_arguments(ctx context.Context, endpoint serverAddress) ([]string, err
}
func spawn_server(ctx context.Context, endpoint serverAddress) error {
// Never create a root-owned server in the invoking user's runtime directory.
if uid := os.Getenv("SUDO_UID"); uid != "" && uid != strconv.Itoa(os.Geteuid()) {
return fmt.Errorf("start Speech Dispatcher as the invoking user before running Fenrir with sudo")
}
spawnContext, cancel := context.WithTimeout(ctx, commandTimeout)
defer cancel()
args, err := spawn_arguments(spawnContext, endpoint)
@@ -117,6 +115,24 @@ func spawn_server(ctx context.Context, endpoint serverAddress) error {
command = "speech-dispatcher"
}
process := exec.CommandContext(spawnContext, command, args...)
if uid := os.Getenv("SUDO_UID"); uid != "" && uid != strconv.Itoa(os.Geteuid()) {
// Drop credentials only in the child. Never run a root-owned daemon in
// another user's session or change the reader's device privileges.
if os.Geteuid() != 0 {
return fmt.Errorf("cannot start Speech Dispatcher as the invoking user without root privileges")
}
account, err := user.LookupId(uid)
if err != nil {
return fmt.Errorf("look up invoking user for Speech Dispatcher: %w", err)
}
groups, err := account.GroupIds()
if err != nil {
return fmt.Errorf("look up invoking user's groups: %w", err)
}
if err := configure_sudo_spawn(process, account, groups); err != nil {
return err
}
}
process.WaitDelay = 100 * time.Millisecond
output, err := process.CombinedOutput()
if err != nil {
@@ -124,3 +140,48 @@ func spawn_server(ctx context.Context, endpoint serverAddress) error {
}
return nil
}
func configure_sudo_spawn(process *exec.Cmd, account *user.User, groups []string) error {
uid, uidErr := strconv.ParseUint(account.Uid, 10, 32)
gid, gidErr := strconv.ParseUint(account.Gid, 10, 32)
if uidErr != nil || gidErr != nil || uid == 0 || !filepath.IsAbs(account.HomeDir) {
return fmt.Errorf("invalid invoking user for Speech Dispatcher")
}
credentials := &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
for _, group := range groups {
id, err := strconv.ParseUint(group, 10, 32)
if err != nil {
return fmt.Errorf("invalid invoking user's group: %w", err)
}
credentials.Groups = append(credentials.Groups, uint32(id))
}
process.SysProcAttr = &syscall.SysProcAttr{Credential: credentials}
process.Dir = account.HomeDir
// Do not pass root's HOME, XDG paths, D-Bus address, or unrelated secrets
// into a user-owned daemon. Preserve explicit audio choices and locale.
process.Env = []string{"HOME=" + account.HomeDir, "USER=" + account.Username,
"LOGNAME=" + account.Username, "PATH=/usr/local/bin:/usr/bin:/bin"}
for _, entry := range os.Environ() {
key, _, _ := strings.Cut(entry, "=")
if key == "LANG" || strings.HasPrefix(key, "LC_") || key == "PULSE_SERVER" || key == "PULSE_COOKIE" {
process.Env = append(process.Env, entry)
}
}
runtimeDir := filepath.Join("/run/user", account.Uid)
if candidate := os.Getenv("XDG_RUNTIME_DIR"); owned_directory(candidate, uint32(uid)) {
runtimeDir = candidate
}
if owned_directory(runtimeDir, uint32(uid)) {
process.Env = append(process.Env, "XDG_RUNTIME_DIR="+runtimeDir)
}
return nil
}
func owned_directory(path string, uid uint32) bool {
info, err := os.Stat(path)
if err != nil || !info.IsDir() {
return false
}
stat, ok := info.Sys().(*syscall.Stat_t)
return ok && stat.Uid == uid
}