All checks were successful
		
		
	
	Test / Create distribution (push) Successful in 33s
				
			Test / Sandbox (push) Successful in 2m9s
				
			Test / Hakurei (push) Successful in 3m6s
				
			Test / Sandbox (race detector) (push) Successful in 3m55s
				
			Test / Hpkg (push) Successful in 4m8s
				
			Test / Hakurei (race detector) (push) Successful in 4m46s
				
			Test / Flake checks (push) Successful in 1m19s
				
			The trailing zero bytes need to be sliced off, so send cookie size alongside buffer content. Signed-off-by: Ophestra <cat@gensokyo.uk>
		
			
				
	
	
		
			211 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import (
 | |
| 	"encoding/gob"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"io/fs"
 | |
| 	"os"
 | |
| 	"strconv"
 | |
| 	"syscall"
 | |
| 
 | |
| 	"hakurei.app/container/check"
 | |
| 	"hakurei.app/hst"
 | |
| 	"hakurei.app/message"
 | |
| )
 | |
| 
 | |
| const pulseCookieSizeMax = 1 << 8
 | |
| 
 | |
| func init() { gob.Register(new(spPulseOp)) }
 | |
| 
 | |
| // spPulseOp exports the PulseAudio server to the container.
 | |
| // Runs after spRuntimeOp.
 | |
| type spPulseOp struct {
 | |
| 	// PulseAudio cookie data, populated during toSystem if a cookie is present.
 | |
| 	Cookie *[pulseCookieSizeMax]byte
 | |
| 	// PulseAudio cookie size, populated during toSystem if a cookie is present.
 | |
| 	CookieSize int
 | |
| }
 | |
| 
 | |
| func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
 | |
| 	if state.et&hst.EPulse == 0 {
 | |
| 		return errNotEnabled
 | |
| 	}
 | |
| 
 | |
| 	pulseRuntimeDir, pulseSocket := s.commonPaths(state.outcomeState)
 | |
| 
 | |
| 	if _, err := state.k.stat(pulseRuntimeDir.String()); err != nil {
 | |
| 		if !errors.Is(err, fs.ErrNotExist) {
 | |
| 			return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
 | |
| 		}
 | |
| 		return newWithMessageError(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir), err)
 | |
| 	}
 | |
| 
 | |
| 	if fi, err := state.k.stat(pulseSocket.String()); err != nil {
 | |
| 		if !errors.Is(err, fs.ErrNotExist) {
 | |
| 			return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
 | |
| 		}
 | |
| 		return newWithMessageError(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir), err)
 | |
| 	} else {
 | |
| 		if m := fi.Mode(); m&0o006 != 0o006 {
 | |
| 			return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// pulse socket is world writable and its parent directory DAC permissions prevents access;
 | |
| 	// hard link to target-executable share directory to grant access
 | |
| 	state.sys.Link(pulseSocket, state.runtime().Append("pulse"))
 | |
| 
 | |
| 	// load up to pulseCookieSizeMax bytes of pulse cookie for transmission to shim
 | |
| 	if a, err := discoverPulseCookie(state.k); err != nil {
 | |
| 		return err
 | |
| 	} else if a != nil {
 | |
| 		s.Cookie = new([pulseCookieSizeMax]byte)
 | |
| 		if s.CookieSize, err = loadFile(state.msg, state.k, "PulseAudio cookie", a.String(), s.Cookie[:]); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	} else {
 | |
| 		state.msg.Verbose("cannot locate PulseAudio cookie (tried " +
 | |
| 			"$PULSE_COOKIE, " +
 | |
| 			"$XDG_CONFIG_HOME/pulse/cookie, " +
 | |
| 			"$HOME/.pulse-cookie)")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *spPulseOp) toContainer(state *outcomeStateParams) error {
 | |
| 	innerPulseSocket := state.runtimeDir.Append("pulse", "native")
 | |
| 	state.params.Bind(state.runtimePath().Append("pulse"), innerPulseSocket, 0)
 | |
| 	state.env["PULSE_SERVER"] = "unix:" + innerPulseSocket.String()
 | |
| 
 | |
| 	if s.Cookie != nil {
 | |
| 		innerDst := hst.AbsPrivateTmp.Append("/pulse-cookie")
 | |
| 
 | |
| 		if s.CookieSize < 0 || s.CookieSize > pulseCookieSizeMax {
 | |
| 			return newWithMessage("unexpected PulseAudio cookie size")
 | |
| 		}
 | |
| 		state.env["PULSE_COOKIE"] = innerDst.String()
 | |
| 		state.params.Place(innerDst, s.Cookie[:s.CookieSize])
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *spPulseOp) commonPaths(state *outcomeState) (pulseRuntimeDir, pulseSocket *check.Absolute) {
 | |
| 	// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
 | |
| 	pulseRuntimeDir = state.sc.RuntimePath.Append("pulse")
 | |
| 	// PulseAudio socket (usually `/run/user/%d/pulse/native`)
 | |
| 	pulseSocket = pulseRuntimeDir.Append("native")
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // discoverPulseCookie attempts to discover the pathname of the PulseAudio cookie of the current user.
 | |
| // If both returned pathname and error are nil, the cookie is likely unavailable and can be silently skipped.
 | |
| func discoverPulseCookie(k syscallDispatcher) (*check.Absolute, error) {
 | |
| 	const paLocateStep = "locate PulseAudio cookie"
 | |
| 
 | |
| 	// from environment
 | |
| 	if p, ok := k.lookupEnv("PULSE_COOKIE"); ok {
 | |
| 		if a, err := check.NewAbs(p); err != nil {
 | |
| 			return nil, &hst.AppError{Step: paLocateStep, Err: err}
 | |
| 		} else {
 | |
| 			// this takes precedence, do not verify whether the file is accessible
 | |
| 			return a, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// $HOME/.pulse-cookie
 | |
| 	if p, ok := k.lookupEnv("HOME"); ok {
 | |
| 		var pulseCookiePath *check.Absolute
 | |
| 		if a, err := check.NewAbs(p); err != nil {
 | |
| 			return nil, &hst.AppError{Step: paLocateStep, Err: err}
 | |
| 		} else {
 | |
| 			pulseCookiePath = a.Append(".pulse-cookie")
 | |
| 		}
 | |
| 
 | |
| 		if fi, err := k.stat(pulseCookiePath.String()); err != nil {
 | |
| 			if !errors.Is(err, fs.ErrNotExist) {
 | |
| 				return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
 | |
| 			}
 | |
| 			// fallthrough
 | |
| 		} else if fi.IsDir() {
 | |
| 			// fallthrough
 | |
| 		} else {
 | |
| 			return pulseCookiePath, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// $XDG_CONFIG_HOME/pulse/cookie
 | |
| 	if p, ok := k.lookupEnv("XDG_CONFIG_HOME"); ok {
 | |
| 		var pulseCookiePath *check.Absolute
 | |
| 		if a, err := check.NewAbs(p); err != nil {
 | |
| 			return nil, &hst.AppError{Step: paLocateStep, Err: err}
 | |
| 		} else {
 | |
| 			pulseCookiePath = a.Append("pulse", "cookie")
 | |
| 		}
 | |
| 
 | |
| 		if fi, err := k.stat(pulseCookiePath.String()); err != nil {
 | |
| 			if !errors.Is(err, fs.ErrNotExist) {
 | |
| 				return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
 | |
| 			}
 | |
| 			// fallthrough
 | |
| 		} else if fi.IsDir() {
 | |
| 			// fallthrough
 | |
| 		} else {
 | |
| 			return pulseCookiePath, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// cookie not present
 | |
| 	// not fatal: authentication is disabled
 | |
| 	return nil, nil
 | |
| }
 | |
| 
 | |
| // loadFile reads up to len(buf) bytes from the file at pathname.
 | |
| func loadFile(
 | |
| 	msg message.Msg, k syscallDispatcher,
 | |
| 	description, pathname string, buf []byte,
 | |
| ) (int, error) {
 | |
| 	n := len(buf)
 | |
| 	if n == 0 {
 | |
| 		return -1, errors.New("invalid buffer")
 | |
| 	}
 | |
| 
 | |
| 	if fi, err := k.stat(pathname); err != nil {
 | |
| 		return -1, &hst.AppError{Step: "access " + description, Err: err}
 | |
| 	} else {
 | |
| 		if fi.IsDir() {
 | |
| 			return -1, &hst.AppError{Step: "read " + description,
 | |
| 				Err: &os.PathError{Op: "stat", Path: pathname, Err: syscall.EISDIR}}
 | |
| 		}
 | |
| 		if s := fi.Size(); s > int64(n) {
 | |
| 			return -1, newWithMessageError(
 | |
| 				description+" at "+strconv.Quote(pathname)+" exceeds expected size",
 | |
| 				&os.PathError{Op: "stat", Path: pathname, Err: syscall.ENOMEM},
 | |
| 			)
 | |
| 		} else if s < int64(n) {
 | |
| 			msg.Verbosef("%s at %q is %d bytes shorter than expected", description, pathname, int64(n)-s)
 | |
| 		} else {
 | |
| 			msg.Verbosef("loading %d bytes from %q", n, pathname)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if f, err := k.open(pathname); err != nil {
 | |
| 		return -1, &hst.AppError{Step: "open " + description, Err: err}
 | |
| 	} else {
 | |
| 		if n, err = f.Read(buf); err != nil {
 | |
| 			if !errors.Is(err, io.EOF) {
 | |
| 				_ = f.Close()
 | |
| 				return n, &hst.AppError{Step: "read " + description, Err: err}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err = f.Close(); err != nil {
 | |
| 			return n, &hst.AppError{Step: "close " + description, Err: err}
 | |
| 		}
 | |
| 		return n, nil
 | |
| 	}
 | |
| }
 |