승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
388 lines
12 KiB
Go
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
|
|
}
|