Migrate ordinary I/O from confined file helpers

This commit is contained in:
2026-09-01 17:51:37 +00:00
parent 0d57422b61
commit ba858cc997
34 changed files with 96 additions and 547 deletions

View File

@@ -1,148 +0,0 @@
package fileops
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestOpenFileConfinedRejectsNonRegularLeaf(t *testing.T) {
leaf := filepath.Join(t.TempDir(), "diagnostic.log")
if err := os.Mkdir(leaf, 0o700); err != nil {
t.Fatalf("Mkdir() error = %v", err)
}
file, err := OpenFileConfined(leaf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, WorkspaceFileMode)
if file != nil {
_ = file.Close()
t.Fatal("OpenFileConfined() returned a file for a directory")
}
if err == nil || !strings.Contains(err.Error(), "not a regular file") {
t.Fatalf("OpenFileConfined() error = %v, want regular-file rejection", err)
}
}
func TestOpenFileConfinedRejectsSymlinkWithoutTruncatingTarget(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("creating symlinks requires privileges that are not available on every Windows runner")
}
dir := t.TempDir()
target := filepath.Join(dir, "target.log")
const original = "outside content"
if err := os.WriteFile(target, []byte(original), 0o600); err != nil {
t.Fatalf("WriteFile(target) error = %v", err)
}
leaf := filepath.Join(dir, "diagnostic.log")
if err := os.Symlink(target, leaf); err != nil {
t.Fatalf("Symlink() error = %v", err)
}
file, err := OpenFileConfined(leaf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, WorkspaceFileMode)
if file != nil {
_ = file.Close()
t.Fatal("OpenFileConfined() returned a file for a symbolic link")
}
if err == nil || !strings.Contains(err.Error(), "symbolic link") {
t.Fatalf("OpenFileConfined() error = %v, want symbolic-link rejection", err)
}
data, readErr := os.ReadFile(target)
if readErr != nil {
t.Fatalf("ReadFile(target) error = %v", readErr)
}
if string(data) != original {
t.Fatalf("target content = %q, want %q", data, original)
}
}
func TestOpenFileConfinedDoesNotFollowLeafReplacedBeforeTruncate(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("creating symlinks requires privileges that are not available on every Windows runner")
}
dir := t.TempDir()
target := filepath.Join(dir, "target.log")
const original = "unrelated content"
if err := os.WriteFile(target, []byte(original), 0o600); err != nil {
t.Fatalf("WriteFile(target) error = %v", err)
}
leaf := filepath.Join(dir, "diagnostic.log")
if err := os.WriteFile(leaf, []byte("old diagnostic"), 0o600); err != nil {
t.Fatalf("WriteFile(leaf) error = %v", err)
}
file, err := openFileConfined(leaf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, WorkspaceFileMode, func() error {
if err := os.Remove(leaf); err != nil {
return err
}
return os.Symlink(filepath.Base(target), leaf)
})
if err != nil {
t.Fatalf("openFileConfined() error = %v", err)
}
if _, err := file.WriteString("new diagnostic"); err != nil {
_ = file.Close()
t.Fatalf("WriteString() error = %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
targetData, err := os.ReadFile(target)
if err != nil {
t.Fatalf("ReadFile(target) error = %v", err)
}
if string(targetData) != original {
t.Fatalf("target content = %q, want %q", targetData, original)
}
leafData, err := os.ReadFile(leaf)
if err != nil {
t.Fatalf("ReadFile(leaf) error = %v", err)
}
if string(leafData) != "new diagnostic" {
t.Fatalf("leaf content = %q, want replacement diagnostic", leafData)
}
info, err := os.Lstat(leaf)
if err != nil {
t.Fatalf("Lstat(leaf) error = %v", err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
t.Fatalf("leaf mode = %v, want regular file", info.Mode())
}
}
func TestOpenFileConfinedRejectsLeafReplacedBeforeExistingFileOpen(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("creating symlinks requires privileges that are not available on every Windows runner")
}
dir := t.TempDir()
target := filepath.Join(dir, "target.lock")
const original = "unrelated lock content"
if err := os.WriteFile(target, []byte(original), 0o600); err != nil {
t.Fatalf("WriteFile(target) error = %v", err)
}
leaf := filepath.Join(dir, "session.lock")
if err := os.WriteFile(leaf, []byte("old lock"), 0o600); err != nil {
t.Fatalf("WriteFile(leaf) error = %v", err)
}
file, err := openFileConfined(leaf, os.O_RDWR|os.O_CREATE, WorkspaceFileMode, func() error {
if err := os.Remove(leaf); err != nil {
return err
}
return os.Symlink(filepath.Base(target), leaf)
})
if file != nil {
_ = file.Close()
t.Fatal("openFileConfined() returned a raced symbolic-link target")
}
if err == nil || !strings.Contains(err.Error(), "changed while being opened") {
t.Fatalf("openFileConfined() error = %v, want identity-change rejection", err)
}
targetData, readErr := os.ReadFile(target)
if readErr != nil {
t.Fatalf("ReadFile(target) error = %v", readErr)
}
if string(targetData) != original {
t.Fatalf("target content = %q, want %q", targetData, original)
}
}

View File

@@ -1,78 +0,0 @@
package fileops
import (
"fmt"
"os"
"path/filepath"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
// OpenConfinedRegularFile opens a declared file beneath root without following
// symlinked ancestors or leaf entries. The returned descriptor remains valid if
// the pathname is later replaced.
func OpenConfinedRegularFile(rootPath, relativePath string) (*os.File, error) {
rootPath = filepath.Clean(strings.TrimSpace(rootPath))
if rootPath == "" || rootPath == "." {
return nil, fmt.Errorf("source root is required")
}
relativePath, err := pathsafe.NormalizeRelativeDestination(relativePath)
if err != nil {
return nil, fmt.Errorf("source path: %w", err)
}
root, err := openConfinedDirectory(rootPath)
if err != nil {
return nil, fmt.Errorf("open source root: %w", err)
}
defer root.Close()
parts := strings.Split(filepath.FromSlash(relativePath), string(filepath.Separator))
parent := root
for _, part := range parts[:len(parts)-1] {
child, err := openConfinedChild(parent, part, false, 0)
if err != nil {
if parent != root {
_ = parent.Close()
}
return nil, fmt.Errorf("open source ancestor %q: %w", part, err)
}
if parent != root {
_ = parent.Close()
}
parent = child
}
if parent != root {
defer parent.Close()
}
name := parts[len(parts)-1]
declared, err := parent.Lstat(name)
if err != nil {
return nil, fmt.Errorf("inspect source %q: %w", relativePath, err)
}
if declared.Mode()&os.ModeSymlink != 0 || !declared.Mode().IsRegular() {
return nil, fmt.Errorf("source %q is not a regular file", relativePath)
}
file, err := parent.Open(name)
if err != nil {
return nil, fmt.Errorf("open source %q: %w", relativePath, err)
}
opened, err := file.Stat()
if err != nil {
_ = file.Close()
return nil, fmt.Errorf("inspect opened source %q: %w", relativePath, err)
}
current, err := parent.Lstat(name)
if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() ||
!opened.Mode().IsRegular() || !os.SameFile(opened, declared) || !os.SameFile(opened, current) {
_ = file.Close()
if err != nil {
return nil, fmt.Errorf("reinspect source %q: %w", relativePath, err)
}
return nil, fmt.Errorf("source %q changed while being opened", relativePath)
}
return file, nil
}

View File

@@ -1,121 +0,0 @@
package fileops
import (
"fmt"
"io"
"os"
"path/filepath"
)
// ValidateConfinedDirectory opens path without following symbolic links and
// applies validate to the opened directory's metadata.
func ValidateConfinedDirectory(path string, validate func(os.FileInfo) error) error {
root, err := openConfinedDirectory(path)
if err != nil {
return err
}
defer func() { _ = root.Close() }()
info, err := root.Stat(".")
if err != nil {
return fmt.Errorf("inspect root directory: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("root path is not a directory")
}
if validate != nil {
return validate(info)
}
return nil
}
// ReadRegularFileUnderRoot reads a bounded regular file after opening root and
// every relative-path ancestor without following symbolic links. validateRoot
// and validateFile may enforce caller-owned access policy while their handles
// are still verified.
func ReadRegularFileUnderRoot(
rootPath, relativePath string,
maxBytes int64,
validateRoot, validateFile func(os.FileInfo) error,
) ([]byte, error) {
if maxBytes < 0 {
return nil, fmt.Errorf("maximum byte count must not be negative")
}
if filepath.IsAbs(relativePath) {
return nil, fmt.Errorf("relative file path must not be absolute")
}
parts, err := relativePathParts(relativePath)
if err != nil {
return nil, fmt.Errorf("invalid relative file path: %w", err)
}
root, err := openConfinedDirectory(rootPath)
if err != nil {
return nil, err
}
defer func() { _ = root.Close() }()
rootInfo, err := root.Stat(".")
if err != nil {
return nil, fmt.Errorf("inspect root directory: %w", err)
}
if validateRoot != nil {
if err := validateRoot(rootInfo); err != nil {
return nil, err
}
}
for _, part := range parts[:len(parts)-1] {
child, err := openConfinedChild(root, part, false, 0)
if err != nil {
return nil, err
}
_ = root.Close()
root = child
}
name := parts[len(parts)-1]
inspected, err := root.Lstat(name)
if err != nil {
return nil, fmt.Errorf("inspect file %q: %w", name, err)
}
if inspected.Mode()&os.ModeSymlink != 0 || !inspected.Mode().IsRegular() {
return nil, fmt.Errorf("file %q is not a regular file", name)
}
file, err := root.Open(name)
if err != nil {
return nil, fmt.Errorf("open file %q: %w", name, err)
}
defer func() { _ = file.Close() }()
opened, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("inspect opened file %q: %w", name, err)
}
current, err := root.Lstat(name)
if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() || !os.SameFile(opened, current) {
if err != nil {
return nil, fmt.Errorf("reinspect file %q: %w", name, err)
}
return nil, fmt.Errorf("file %q changed while being opened", name)
}
if validateFile != nil {
if err := validateFile(opened); err != nil {
return nil, err
}
}
content, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
if err != nil {
return nil, fmt.Errorf("read file %q: %w", name, err)
}
if int64(len(content)) > maxBytes {
return nil, fmt.Errorf("file %q exceeds %d-byte limit", name, maxBytes)
}
return content, nil
}
// ReadRegularFile reads a bounded regular file without following symbolic links
// in its parent hierarchy. It is intended for externally produced results;
// callers own the limit and semantic validation contract.
func ReadRegularFile(path string, maxBytes int64) ([]byte, error) {
clean := filepath.Clean(path)
if clean == "." || filepath.Base(clean) == "." {
return nil, fmt.Errorf("file path is required")
}
return ReadRegularFileUnderRoot(filepath.Dir(clean), filepath.Base(clean), maxBytes, nil, nil)
}

View File

@@ -1,65 +0,0 @@
package fileops
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestReadRegularFileEnforcesLimitAndRegularFileIdentity(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "result.json")
if err := os.WriteFile(path, []byte("1234"), 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
data, err := ReadRegularFile(path, 4)
if err != nil {
t.Fatalf("ReadRegularFile(exact limit) error = %v", err)
}
if string(data) != "1234" {
t.Fatalf("data = %q, want exact file content", data)
}
if _, err := ReadRegularFile(path, 3); err == nil || !strings.Contains(err.Error(), "3-byte limit") {
t.Fatalf("ReadRegularFile(limit plus one) error = %v, want limit failure", err)
}
if err := os.Remove(path); err != nil {
t.Fatalf("Remove(file) error = %v", err)
}
if err := os.Mkdir(path, 0o700); err != nil {
t.Fatalf("Mkdir(non-regular file) error = %v", err)
}
if _, err := ReadRegularFile(path, 4); err == nil || !strings.Contains(err.Error(), "not a regular file") {
t.Fatalf("ReadRegularFile(directory) error = %v, want regular-file failure", err)
}
}
func TestReadRegularFileRejectsSymlinkedPaths(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink behavior differs on windows")
}
root := t.TempDir()
outside := t.TempDir()
outsideFile := filepath.Join(outside, "result.json")
if err := os.WriteFile(outsideFile, []byte("outside"), 0o600); err != nil {
t.Fatalf("WriteFile(outside) error = %v", err)
}
leaf := filepath.Join(root, "result.json")
if err := os.Symlink(outsideFile, leaf); err != nil {
t.Fatalf("Symlink(leaf) error = %v", err)
}
if _, err := ReadRegularFile(leaf, 64); err == nil {
t.Fatal("ReadRegularFile(leaf symlink) error = nil, want rejection")
}
if err := os.Remove(leaf); err != nil {
t.Fatalf("Remove(leaf symlink) error = %v", err)
}
linkedParent := filepath.Join(root, "linked")
if err := os.Symlink(outside, linkedParent); err != nil {
t.Fatalf("Symlink(parent) error = %v", err)
}
if _, err := ReadRegularFile(filepath.Join(linkedParent, "result.json"), 64); err == nil {
t.Fatal("ReadRegularFile(symlinked parent) error = nil, want rejection")
}
}