iop/apps/node/internal/workspace/cleanup_path_unix.go
toki dc9a9a8c59 feat(agent): 단일 요청 Agent 실행 경계를 구현한다
승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
2026-08-07 07:03:55 +09:00

388 lines
12 KiB
Go

//go:build darwin || linux
package workspace
import (
"errors"
"io"
"os"
"path"
"strings"
"golang.org/x/sys/unix"
)
func initializeRequestArtifacts(entry *catalogEntry, requestID string) (map[string]ownedArtifact, []ownedArtifact, error) {
if entry == nil || entry.directory == nil || !validRequestID(requestID) {
return nil, nil, errUnsafePath
}
fd, err := duplicateDirectory(entry.directory)
if err != nil {
return nil, nil, err
}
defer unix.Close(fd)
components := []string{".iop", "job", requestID}
created := make([]ownedArtifact, 0, len(components))
relative := ""
current := fd
for index, component := range components {
if relative == "" {
relative = component
} else {
relative = path.Join(relative, component)
}
stat, statErr := statNoFollow(current, component)
wasCreated := false
if errors.Is(statErr, unix.ENOENT) {
if err := unix.Mkdirat(current, component, 0o700); err != nil {
rollbackOwnedArtifacts(entry, created)
return nil, nil, errUnsafePath
}
wasCreated = true
stat, statErr = statNoFollow(current, component)
}
if statErr != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != entry.device || index == len(components)-1 && !wasCreated {
rollbackOwnedArtifacts(entry, created)
return nil, nil, errUnsafePath
}
next, openErr := openDirectoryAt(current, component, entry.device)
if openErr != nil {
rollbackOwnedArtifacts(entry, created)
return nil, nil, openErr
}
opened, identityErr := descriptorArtifact(next, relative, ownedArtifactDirectory, entry.device)
if identityErr != nil || opened.device != uint64(stat.Dev) || opened.inode != uint64(stat.Ino) {
unix.Close(next)
rollbackOwnedArtifacts(entry, created)
return nil, nil, errUnsafePath
}
if wasCreated {
created = append(created, opened)
}
if current != fd {
unix.Close(current)
}
current = next
}
if current != fd {
unix.Close(current)
}
requestRoot := ".iop/job/" + requestID
artifacts := map[string]ownedArtifact{requestRoot: created[len(created)-1]}
parents := make([]ownedArtifact, 0, 2)
for _, artifact := range created[:len(created)-1] {
parents = append(parents, artifact)
}
return artifacts, parents, nil
}
func createOwnedArtifact(entry *catalogEntry, requestRoot, relative string, content []byte, inventory map[string]ownedArtifact) ([]ownedArtifact, error) {
rootIdentity, ok := inventory[requestRoot]
if !ok || rootIdentity.kind != ownedArtifactDirectory {
return nil, errUnsafePath
}
fd, err := openDirectoryPath(entry, requestRoot)
if err != nil {
return nil, err
}
defer unix.Close(fd)
currentIdentity, err := descriptorArtifact(fd, requestRoot, ownedArtifactDirectory, entry.device)
if err != nil || currentIdentity != rootIdentity {
return nil, errUnsafePath
}
parts := strings.Split(relative, "/")
created := make([]ownedArtifact, 0, len(parts))
currentPath := requestRoot
current := fd
for _, component := range parts[:len(parts)-1] {
currentPath = path.Join(currentPath, component)
stat, statErr := statNoFollow(current, component)
if errors.Is(statErr, unix.ENOENT) {
if err := unix.Mkdirat(current, component, 0o700); err != nil {
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
stat, statErr = statNoFollow(current, component)
if statErr != nil {
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
created = append(created, ownedArtifact{relative: currentPath, kind: ownedArtifactDirectory, device: uint64(stat.Dev), inode: uint64(stat.Ino)})
}
owned, admitted := inventory[currentPath]
if !admitted {
for _, candidate := range created {
if candidate.relative == currentPath {
owned, admitted = candidate, true
break
}
}
}
if statErr != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != entry.device || !admitted || owned.kind != ownedArtifactDirectory || owned.device != uint64(stat.Dev) || owned.inode != uint64(stat.Ino) {
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
next, openErr := openDirectoryAt(current, component, entry.device)
if openErr != nil {
rollbackOwnedArtifacts(entry, created)
return nil, openErr
}
if current != fd {
unix.Close(current)
}
current = next
}
if current != fd {
defer unix.Close(current)
}
base := parts[len(parts)-1]
if _, statErr := statNoFollow(current, base); !errors.Is(statErr, unix.ENOENT) {
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
fileFD, err := unix.Openat(current, base, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600)
if err != nil {
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
file := os.NewFile(uintptr(fileFD), base)
written := false
defer func() {
if !written {
_ = unix.Unlinkat(current, base, 0)
}
}()
if _, err := file.Write(content); err != nil {
file.Close()
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
if err := file.Sync(); err != nil {
file.Close()
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
var stat unix.Stat_t
if err := unix.Fstat(fileFD, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFREG || uint64(stat.Dev) != entry.device {
file.Close()
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
if err := file.Close(); err != nil {
rollbackOwnedArtifacts(entry, created)
return nil, errUnsafePath
}
written = true
created = append(created, ownedArtifact{
relative: path.Join(requestRoot, relative), kind: ownedArtifactFile,
device: uint64(stat.Dev), inode: uint64(stat.Ino),
})
return created, nil
}
func validateAndRemoveOwnedArtifacts(entry *catalogEntry, requestRoot string, inventory map[string]ownedArtifact, ownedParents []ownedArtifact) (int, error) {
if err := validateOwnedTree(entry, requestRoot, inventory); err != nil {
return 0, err
}
removed := 0
for _, artifact := range sortedArtifactsDeepestFirst(inventory) {
if err := removeExactArtifact(entry, artifact, false); err != nil {
return removed, err
}
removed++
}
for index := len(ownedParents) - 1; index >= 0; index-- {
if err := removeExactArtifact(entry, ownedParents[index], true); err != nil {
return removed, err
}
}
return removed, nil
}
func validateOwnedTree(entry *catalogEntry, requestRoot string, inventory map[string]ownedArtifact) error {
rootIdentity, ok := inventory[requestRoot]
if !ok || rootIdentity.kind != ownedArtifactDirectory {
return errUnsafePath
}
fd, err := openDirectoryPath(entry, requestRoot)
if err != nil {
return err
}
defer unix.Close(fd)
opened, err := descriptorArtifact(fd, requestRoot, ownedArtifactDirectory, entry.device)
if err != nil || opened != rootIdentity {
return errUnsafePath
}
seen := map[string]struct{}{requestRoot: {}}
if err := enumerateOwnedDirectory(entry, fd, requestRoot, inventory, seen); err != nil {
return err
}
if len(seen) != len(inventory) {
return errUnsafePath
}
return nil
}
func enumerateOwnedDirectory(entry *catalogEntry, fd int, relative string, inventory map[string]ownedArtifact, seen map[string]struct{}) error {
dup, err := unix.Dup(fd)
if err != nil {
return errUnsafePath
}
unix.CloseOnExec(dup)
directory := os.NewFile(uintptr(dup), relative)
defer directory.Close()
for {
entries, readErr := directory.ReadDir(128)
for _, entryValue := range entries {
name := entryValue.Name()
if name == "" || name == "." || name == ".." || strings.Contains(name, "/") {
return errUnsafePath
}
childPath := path.Join(relative, name)
stat, err := statNoFollow(fd, name)
if err != nil || uint64(stat.Dev) != entry.device {
return errUnsafePath
}
kind := ownedArtifactFile
switch stat.Mode & unix.S_IFMT {
case unix.S_IFREG:
case unix.S_IFDIR:
kind = ownedArtifactDirectory
default:
return errUnsafePath
}
owned, ok := inventory[childPath]
if !ok || owned.kind != kind || owned.device != uint64(stat.Dev) || owned.inode != uint64(stat.Ino) {
return errUnsafePath
}
seen[childPath] = struct{}{}
if kind == ownedArtifactDirectory {
childFD, err := openDirectoryAt(fd, name, entry.device)
if err != nil {
return err
}
opened, identityErr := descriptorArtifact(childFD, childPath, kind, entry.device)
if identityErr != nil || opened != owned {
unix.Close(childFD)
return errUnsafePath
}
err = enumerateOwnedDirectory(entry, childFD, childPath, inventory, seen)
unix.Close(childFD)
if err != nil {
return err
}
}
}
if errors.Is(readErr, io.EOF) {
break
}
if readErr != nil {
return errUnsafePath
}
}
return nil
}
func removeExactArtifact(entry *catalogEntry, artifact ownedArtifact, ignoreNonEmpty bool) error {
parentPath := path.Dir(artifact.relative)
base := path.Base(artifact.relative)
parentFD, err := openDirectoryPath(entry, parentPath)
if err != nil {
return err
}
defer unix.Close(parentFD)
stat, err := statNoFollow(parentFD, base)
if err != nil || uint64(stat.Dev) != artifact.device || uint64(stat.Ino) != artifact.inode {
return errUnsafePath
}
flags := 0
wantMode := uint32(unix.S_IFREG)
if artifact.kind == ownedArtifactDirectory {
flags = unix.AT_REMOVEDIR
wantMode = unix.S_IFDIR
}
if uint32(stat.Mode)&uint32(unix.S_IFMT) != wantMode {
return errUnsafePath
}
if err := unix.Unlinkat(parentFD, base, flags); err != nil {
if ignoreNonEmpty && (errors.Is(err, unix.ENOTEMPTY) || errors.Is(err, unix.EEXIST)) {
return nil
}
return errUnsafePath
}
return nil
}
func rollbackOwnedArtifacts(entry *catalogEntry, artifacts []ownedArtifact) {
for index := len(artifacts) - 1; index >= 0; index-- {
_ = removeExactArtifact(entry, artifacts[index], true)
}
}
func duplicateDirectory(directory *os.File) (int, error) {
if directory == nil {
return -1, errUnsafePath
}
fd, err := unix.Dup(int(directory.Fd()))
if err != nil {
return -1, errUnsafePath
}
unix.CloseOnExec(fd)
return fd, nil
}
func openDirectoryPath(entry *catalogEntry, relative string) (int, error) {
fd, err := duplicateDirectory(entry.directory)
if err != nil {
return -1, err
}
if relative == "." {
return fd, nil
}
for _, component := range strings.Split(relative, "/") {
next, openErr := openDirectoryAt(fd, component, entry.device)
unix.Close(fd)
if openErr != nil {
return -1, openErr
}
fd = next
}
return fd, nil
}
func openDirectoryAt(parent int, name string, device uint64) (int, error) {
fd, err := unix.Openat(parent, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
return -1, errUnsafePath
}
var stat unix.Stat_t
if err := unix.Fstat(fd, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != device {
unix.Close(fd)
return -1, errUnsafePath
}
return fd, nil
}
func statNoFollow(parent int, name string) (unix.Stat_t, error) {
var stat unix.Stat_t
err := unix.Fstatat(parent, name, &stat, unix.AT_SYMLINK_NOFOLLOW)
return stat, err
}
func descriptorArtifact(fd int, relative string, kind ownedArtifactKind, device uint64) (ownedArtifact, error) {
var stat unix.Stat_t
if err := unix.Fstat(fd, &stat); err != nil || uint64(stat.Dev) != device {
return ownedArtifact{}, errUnsafePath
}
wantMode := uint32(unix.S_IFREG)
if kind == ownedArtifactDirectory {
wantMode = unix.S_IFDIR
}
if uint32(stat.Mode)&uint32(unix.S_IFMT) != wantMode {
return ownedArtifact{}, errUnsafePath
}
return ownedArtifact{relative: relative, kind: kind, device: uint64(stat.Dev), inode: uint64(stat.Ino)}, nil
}