fortify/helper/proc/files.go
Ophestra 60c2873750
All checks were successful
Test / Create distribution (push) Successful in 1m31s
Test / Run NixOS test (push) Successful in 4m13s
helper/proc: cancel ec on parent ctx
This allows errors written during a timeout to be received and handled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 23:08:28 +09:00

153 lines
3.8 KiB
Go

package proc
import (
"context"
"os"
"os/exec"
"sync/atomic"
"syscall"
"time"
)
var FulfillmentTimeout = 2 * time.Second
// A File is an extra file with deferred initialisation.
type File interface {
// Init initialises File state. Init must not be called more than once.
Init(fd uintptr, v **os.File) uintptr
// Fd returns the fd value set on initialisation.
Fd() uintptr
// ErrCount returns count of error values emitted during fulfillment.
ErrCount() int
// Fulfill is called prior to process creation and must populate its corresponding file address.
// Calls to dispatchErr must match the return value of ErrCount.
// Fulfill must not be called more than once.
Fulfill(ctx context.Context, dispatchErr func(error)) error
}
// ExtraFilesPre is a linked list storing addresses of [os.File].
type ExtraFilesPre struct {
n *ExtraFilesPre
v *os.File
}
// Append grows the list by one entry and returns an address of the address of [os.File] stored in the new entry.
func (f *ExtraFilesPre) Append() (uintptr, **os.File) { return f.append(3) }
// Files returns a slice pointing to a continuous segment of memory containing all addresses stored in f in order.
func (f *ExtraFilesPre) Files() []*os.File { return f.copy(make([]*os.File, 0, f.len())) }
func (f *ExtraFilesPre) append(i uintptr) (uintptr, **os.File) {
if f.n == nil {
f.n = new(ExtraFilesPre)
return i, &f.v
}
return f.n.append(i + 1)
}
func (f *ExtraFilesPre) len() uintptr {
if f == nil {
return 0
}
return f.n.len() + 1
}
func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
if f == nil {
// the public methods ensure the first call is never nil;
// the last element is unused, slice it off here
return e[:len(e)-1]
}
return f.n.copy(append(e, f.v))
}
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *ExtraFilesPre) (err error) {
var ecs int
for _, o := range files {
ecs += o.ErrCount()
}
ec := make(chan error, ecs)
c, cancel := context.WithTimeout(ctx, FulfillmentTimeout)
defer cancel()
for _, f := range files {
err = f.Fulfill(c, makeDispatchErr(f, ec))
if err != nil {
return
}
}
cmd.ExtraFiles = extraFiles.Files()
if err = cmd.Start(); err != nil {
return
}
for ecs > 0 {
select {
case err = <-ec:
ecs--
if err != nil {
break
}
case <-ctx.Done():
err = syscall.ECANCELED
break
}
}
return
}
// InitFile initialises f as part of the slice extraFiles points to,
// and returns its final fd value.
func InitFile(f File, extraFiles *ExtraFilesPre) (fd uintptr) { return f.Init(extraFiles.Append()) }
// BaseFile implements the Init method of the File interface and provides indirect access to extra file state.
type BaseFile struct {
fd uintptr
v **os.File
}
func (f *BaseFile) Init(fd uintptr, v **os.File) uintptr {
if v == nil || fd < 3 {
panic("invalid extra file initial state")
}
if f.v != nil {
panic("extra file initialised twice")
}
f.fd, f.v = fd, v
return fd
}
func (f *BaseFile) Fd() uintptr {
if f.v == nil {
panic("use of uninitialised extra file")
}
return f.fd
}
func (f *BaseFile) Set(v *os.File) {
*f.v = v // runtime guards against use before init
}
func makeDispatchErr(f File, ec chan<- error) func(error) {
c := new(atomic.Int32)
c.Store(int32(f.ErrCount()))
return func(err error) {
if c.Add(-1) < 0 {
panic("unexpected error dispatches")
}
ec <- err
}
}
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr) {
return ExtraFileSlice(&cmd.ExtraFiles, f)
}
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr) {
// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
fd = uintptr(3 + len(*extraFiles))
*extraFiles = append(*extraFiles, f)
return
}