//go:build linux package hostfloor import ( "errors" "fmt" "path/filepath" "golang.org/x/sys/unix" "github.com/carlosframework/platform/internal/safefile" ) // EnsureControllerDir creates /.hostexec, the claim controllers' // state directory (claim D6), and settles an existing one: a real directory // owned by uid:gid, mode 2770, like /data/hostguard. The floor-armed update // path refuses while the directory is missing (CheckControllersIdle), so // guard-arm creates it (Opus re-review M1-2). // // Neither dataDir nor .hostexec may be a symlink. Ownership and mode are set // through the opened descriptor, so a path swapped after the open changes // nothing but the directory that was checked. Existing records are kept. func EnsureControllerDir(dataDir string, uid, gid int) error { if !filepath.IsAbs(dataDir) || filepath.Clean(dataDir) != dataDir { return ErrFloor } parentDir, err := safefile.OpenDir(dataDir) if err != nil { return fmt.Errorf("open %s: %w", dataDir, err) } defer parentDir.Close() parent := int(parentDir.Fd()) if err = unix.Mkdirat(parent, ".hostexec", 0o700); err != nil && !errors.Is(err, unix.EEXIST) { return fmt.Errorf("create %s/.hostexec: %w", dataDir, err) } fd, err := unix.Openat(parent, ".hostexec", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return fmt.Errorf("%w: %s/.hostexec is not a directory: %v", ErrFloor, dataDir, err) } defer unix.Close(fd) var st unix.Stat_t if err = unix.Fstat(fd, &st); err != nil { return err } if st.Mode&unix.S_IFMT != unix.S_IFDIR { return ErrFloor } if err = unix.Fchown(fd, uid, gid); err != nil { return err } if err = unix.Fchmod(fd, 0o2770); err != nil { return err } if err = unix.Fsync(fd); err != nil { return err } return unix.Fsync(parent) }