Close diagnostic and restore coverage gaps
This commit is contained in:
@@ -156,13 +156,17 @@ func setOpenedDirectoryMode(root *os.Root, mode os.FileMode) error {
|
||||
}
|
||||
|
||||
func createSiblingTemp(parent *os.Root, base string) (*os.File, string, error) {
|
||||
return createSiblingTempWithFlags(parent, base, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
}
|
||||
|
||||
func createSiblingTempWithFlags(parent *os.Root, base string, flags int, mode os.FileMode) (*os.File, string, error) {
|
||||
for attempt := 0; attempt < 100; attempt++ {
|
||||
var randomBytes [16]byte
|
||||
if _, err := rand.Read(randomBytes[:]); err != nil {
|
||||
return nil, "", fmt.Errorf("generate temporary file name: %w", err)
|
||||
}
|
||||
name := "." + base + ".tmp-" + hex.EncodeToString(randomBytes[:])
|
||||
file, err := parent.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
file, err := parent.OpenFile(name, flags|os.O_CREATE|os.O_EXCL, mode)
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
continue
|
||||
}
|
||||
@@ -184,28 +188,128 @@ func syncOpenedDirectory(parent *os.Root) error {
|
||||
}
|
||||
|
||||
// OpenFileConfined opens a file after verifying its parent hierarchy without
|
||||
// following symbolic links. Existing symbolic-link and non-regular leaves are
|
||||
// mutating through symbolic links. Truncating opens atomically replace the leaf
|
||||
// with a newly created sibling, while other creating opens never carry O_CREATE
|
||||
// into an existing-leaf open. Existing symbolic-link and non-regular leaves are
|
||||
// rejected.
|
||||
func OpenFileConfined(path string, flags int, mode os.FileMode) (*os.File, error) {
|
||||
return openFileConfined(path, flags, mode, nil)
|
||||
}
|
||||
|
||||
func openFileConfined(path string, flags int, mode os.FileMode, beforeOpen func() error) (*os.File, error) {
|
||||
parent, name, err := openConfinedParent(path, false, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = parent.Close() }()
|
||||
if info, err := parent.Lstat(name); err == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, fmt.Errorf("destination file %q is a symbolic link", name)
|
||||
|
||||
if flags&(os.O_CREATE|os.O_EXCL) == os.O_CREATE|os.O_EXCL {
|
||||
if err := runBeforeConfinedOpen(beforeOpen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("destination file %q is not a regular file", name)
|
||||
file, err := parent.OpenFile(name, flags, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("inspect destination file %q: %w", name, err)
|
||||
return validateOpenedConfinedFile(parent, name, file)
|
||||
}
|
||||
file, err := parent.OpenFile(name, flags, mode)
|
||||
|
||||
exists, err := validateConfinedLeaf(parent, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if flags&os.O_TRUNC != 0 {
|
||||
if !exists && flags&os.O_CREATE == 0 {
|
||||
return nil, fmt.Errorf("open destination file %q: %w", name, os.ErrNotExist)
|
||||
}
|
||||
return replaceAndOpenConfinedFile(parent, name, flags, mode, beforeOpen)
|
||||
}
|
||||
|
||||
if err := runBeforeConfinedOpen(beforeOpen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists && flags&os.O_CREATE != 0 {
|
||||
file, err := parent.OpenFile(name, flags|os.O_EXCL, mode)
|
||||
if err == nil {
|
||||
return validateOpenedConfinedFile(parent, name, file)
|
||||
}
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
return nil, err
|
||||
}
|
||||
if _, validationErr := validateConfinedLeaf(parent, name); validationErr != nil {
|
||||
return nil, validationErr
|
||||
}
|
||||
}
|
||||
|
||||
// The leaf now exists, so removing O_CREATE prevents a raced dangling
|
||||
// symlink from creating its target. Opening an existing symlink can expose a
|
||||
// handle, but no caller can mutate through it before the identity check below.
|
||||
file, err := parent.OpenFile(name, flags&^os.O_CREATE, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return validateOpenedConfinedFile(parent, name, file)
|
||||
}
|
||||
|
||||
func validateConfinedLeaf(parent *os.Root, name string) (bool, error) {
|
||||
info, err := parent.Lstat(name)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("inspect destination file %q: %w", name, err)
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return false, fmt.Errorf("destination file %q is a symbolic link", name)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return false, fmt.Errorf("destination file %q is not a regular file", name)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func replaceAndOpenConfinedFile(parent *os.Root, name string, flags int, mode os.FileMode, beforeOpen func() error) (_ *os.File, resultErr error) {
|
||||
temporary, temporaryName, err := createSiblingTempWithFlags(parent, name, flags&^os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create replacement for destination file %q: %w", name, err)
|
||||
}
|
||||
installed := false
|
||||
defer func() {
|
||||
if installed {
|
||||
return
|
||||
}
|
||||
closeErr := temporary.Close()
|
||||
removeErr := parent.Remove(temporaryName)
|
||||
if errors.Is(removeErr, os.ErrNotExist) {
|
||||
removeErr = nil
|
||||
}
|
||||
resultErr = errors.Join(resultErr, closeErr, removeErr)
|
||||
}()
|
||||
|
||||
if err := runBeforeConfinedOpen(beforeOpen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Rename replaces the directory entry itself rather than following it. A
|
||||
// leaf changed to a symlink after validation therefore cannot redirect the
|
||||
// truncation to its target.
|
||||
if err := parent.Rename(temporaryName, name); err != nil {
|
||||
return nil, fmt.Errorf("install replacement for destination file %q: %w", name, err)
|
||||
}
|
||||
installed = true
|
||||
return validateOpenedConfinedFile(parent, name, temporary)
|
||||
}
|
||||
|
||||
func runBeforeConfinedOpen(beforeOpen func() error) error {
|
||||
if beforeOpen == nil {
|
||||
return nil
|
||||
}
|
||||
if err := beforeOpen(); err != nil {
|
||||
return fmt.Errorf("check before confined file open: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOpenedConfinedFile(parent *os.Root, name string, file *os.File) (*os.File, error) {
|
||||
opened, err := file.Stat()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
|
||||
@@ -54,3 +54,95 @@ func TestOpenFileConfinedRejectsSymlinkWithoutTruncatingTarget(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user