package host import ( "bytes" "errors" "io" "os" "path/filepath" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/rootstate" "github.com/carlosframework/platform/internal/safefile" "golang.org/x/sys/unix" ) type claimUnitFiles struct{ dir string } func (f claimUnitFiles) read(unit string) ([]byte, rootstate.FileIdentity, error) { if !instancectl.ValidBoundClaimUnit(unit) { return nil, rootstate.FileIdentity{}, os.ErrPermission } file, err := rootstate.OpenTrustedFile(filepath.Join(f.dir, unit)) if err != nil { return nil, rootstate.FileIdentity{}, err } defer file.Close() raw, err := io.ReadAll(io.LimitReader(file, rootstate.MaxRecord+1)) if err != nil { return nil, rootstate.FileIdentity{}, err } if len(raw) > rootstate.MaxRecord { return nil, rootstate.FileIdentity{}, os.ErrPermission } id, err := rootstate.PersistentIdentity(file) return raw, id, err } func (f claimUnitFiles) create(unit string, body []byte) error { return f.write(unit, rootstate.FileIdentity{}, body) } func (f claimUnitFiles) replace(unit string, id rootstate.FileIdentity, body []byte) error { if id == (rootstate.FileIdentity{}) { return os.ErrPermission } return f.write(unit, id, body) } func (f claimUnitFiles) write(unit string, id rootstate.FileIdentity, body []byte) error { if !instancectl.ValidBoundClaimUnit(unit) { return os.ErrPermission } checked, err := rootstate.Open(f.dir, false, true) if err != nil { return err } defer checked.Close() parent, err := safefile.OpenDir(f.dir) if err != nil { return err } defer parent.Close() tmp, err := os.CreateTemp(f.dir, ".claim-unit-") if err != nil { return err } name := filepath.Base(tmp.Name()) defer unix.Unlinkat(int(parent.Fd()), name, 0) _, err = tmp.Write(body) if err == nil { err = tmp.Chmod(0644) } if err == nil { err = tmp.Sync() } if err = errors.Join(err, tmp.Close()); err != nil { return err } flags := uint(unix.RENAME_NOREPLACE) if id != (rootstate.FileIdentity{}) { _, current, err := f.read(unit) if err != nil { return err } if !current.Same(id) { return os.ErrPermission } flags = 0 } if err = unix.Renameat2(int(parent.Fd()), name, int(parent.Fd()), unit, flags); err != nil { return err } if err = parent.Sync(); err != nil { return err } got, _, err := f.read(unit) if err != nil { return err } if !bytes.Equal(got, body) { return os.ErrPermission } return nil } // The flat name is only a routing projection. Root launch and containment // retain the binding and the private run directory independently of it. func projectClaimBoundSocket(s claimUnitShape) error { if !claimFlatSocket(s) { return nil } parent, err := safefile.OpenDir(filepath.Dir(s.Socket)) if err != nil { return err } defer parent.Close() name := filepath.Base(s.Socket) target := claimSocketTarget(s) if err := unix.Symlinkat(target, int(parent.Fd()), name); err != nil { if !errors.Is(err, unix.EEXIST) { return err } var buf [4096]byte n, err := unix.Readlinkat(int(parent.Fd()), name, buf[:]) if err != nil { return err } if string(buf[:n]) != target { return os.ErrPermission } } return parent.Sync() }