//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 }