Harden bundle promotion against symlink replacement

This commit is contained in:
2026-08-10 02:45:51 +00:00
parent 701b6726d7
commit b99bd38eb4
3 changed files with 535 additions and 284 deletions

View File

@@ -30,6 +30,19 @@ func PromoteDirectory(src, dst string) error {
}
func promoteDirectory(src, dst string, install func(string, string) error) error {
return promoteDirectoryWithHooks(src, dst, install, sourceTraversalHooks{})
}
type sourceTraversalHooks struct {
afterRootInspect func()
afterEntryInspect func(string)
}
func promoteDirectoryWithHooks(
src, dst string,
install func(string, string) error,
hooks sourceTraversalHooks,
) error {
if strings.TrimSpace(src) == "" || strings.TrimSpace(dst) == "" {
return fmt.Errorf("source and destination directory paths are required")
}
@@ -76,7 +89,13 @@ func promoteDirectory(src, dst string, install func(string, string) error) error
}
}()
if err := copyRegularTree(src, temporary); err != nil {
sourceRoot, err := openVerifiedSourceRoot(src, sourceInfo, hooks)
if err != nil {
return err
}
defer func() { _ = sourceRoot.Close() }()
if err := copyRegularTree(sourceRoot, src, temporary, hooks); err != nil {
return err
}
if err := os.Chmod(temporary, promotedDirectoryMode); err != nil {
@@ -96,62 +115,153 @@ func promoteDirectory(src, dst string, install func(string, string) error) error
return nil
}
func copyRegularTree(src, dst string) error {
entries, err := os.ReadDir(src)
func openVerifiedSourceRoot(path string, inspected os.FileInfo, hooks sourceTraversalHooks) (*os.Root, error) {
if hooks.afterRootInspect != nil {
hooks.afterRootInspect()
}
root, err := os.OpenRoot(path)
if err != nil {
return fmt.Errorf("read source directory %q: %w", src, err)
return nil, fmt.Errorf("open source directory %q: %w", path, err)
}
verified := false
defer func() {
if !verified {
_ = root.Close()
}
}()
opened, err := root.Stat(".")
if err != nil {
return nil, fmt.Errorf("inspect opened source directory %q: %w", path, err)
}
if !opened.IsDir() || !os.SameFile(inspected, opened) {
return nil, fmt.Errorf("source directory %q changed while being opened", path)
}
current, err := os.Lstat(path)
if err != nil {
return nil, fmt.Errorf("reinspect source directory %q: %w", path, err)
}
if current.Mode()&os.ModeSymlink != 0 || !current.IsDir() || !os.SameFile(opened, current) {
return nil, fmt.Errorf("source directory %q changed while being opened", path)
}
verified = true
return root, nil
}
func copyRegularTree(src *os.Root, sourcePath, dst string, hooks sourceTraversalHooks) error {
directory, err := src.Open(".")
if err != nil {
return fmt.Errorf("open source directory %q for traversal: %w", sourcePath, err)
}
defer func() { _ = directory.Close() }()
entries, err := directory.ReadDir(-1)
if err != nil {
return fmt.Errorf("read source directory %q: %w", sourcePath, err)
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Name() < entries[j].Name()
})
for _, entry := range entries {
sourcePath := filepath.Join(src, entry.Name())
entryPath := filepath.Join(sourcePath, entry.Name())
destinationPath := filepath.Join(dst, entry.Name())
info, err := os.Lstat(sourcePath)
info, err := src.Lstat(entry.Name())
if err != nil {
return fmt.Errorf("inspect source entry %q: %w", sourcePath, err)
return fmt.Errorf("inspect source entry %q: %w", entryPath, err)
}
switch {
case info.Mode().IsRegular():
if err := copyRegularFile(sourcePath, destinationPath, info); err != nil {
if hooks.afterEntryInspect != nil {
hooks.afterEntryInspect(entryPath)
}
if err := copyRegularFile(src, entry.Name(), entryPath, destinationPath, info); err != nil {
return err
}
case info.IsDir():
if err := os.Mkdir(destinationPath, promotedDirectoryMode); err != nil {
return fmt.Errorf("create destination directory %q: %w", destinationPath, err)
if hooks.afterEntryInspect != nil {
hooks.afterEntryInspect(entryPath)
}
if err := copyRegularTree(sourcePath, destinationPath); err != nil {
if err := copyRegularDirectory(src, entry.Name(), entryPath, destinationPath, info, hooks); err != nil {
return err
}
if err := os.Chmod(destinationPath, promotedDirectoryMode); err != nil {
return fmt.Errorf("set destination directory permissions %q: %w", destinationPath, err)
}
if err := syncDirectory(destinationPath); err != nil {
return fmt.Errorf("sync destination directory %q: %w", destinationPath, err)
}
default:
return fmt.Errorf("source entry %q has unsupported file type %s", sourcePath, info.Mode().Type())
return fmt.Errorf("source entry %q has unsupported file type %s", entryPath, info.Mode().Type())
}
}
return nil
}
func copyRegularFile(src, dst string, inspected os.FileInfo) error {
in, err := os.Open(src)
func copyRegularDirectory(
parent *os.Root,
name, sourcePath, dst string,
inspected os.FileInfo,
hooks sourceTraversalHooks,
) error {
child, err := parent.OpenRoot(name)
if err != nil {
return fmt.Errorf("open source file %q: %w", src, err)
return fmt.Errorf("open source directory %q: %w", sourcePath, err)
}
defer func() { _ = child.Close() }()
opened, err := child.Stat(".")
if err != nil {
return fmt.Errorf("inspect opened source directory %q: %w", sourcePath, err)
}
if !opened.IsDir() || !os.SameFile(inspected, opened) {
return fmt.Errorf("source directory %q changed while being copied", sourcePath)
}
current, err := parent.Lstat(name)
if err != nil {
return fmt.Errorf("reinspect source directory %q: %w", sourcePath, err)
}
if current.Mode()&os.ModeSymlink != 0 || !current.IsDir() || !os.SameFile(opened, current) {
return fmt.Errorf("source directory %q changed while being copied", sourcePath)
}
if err := os.Mkdir(dst, promotedDirectoryMode); err != nil {
return fmt.Errorf("create destination directory %q: %w", dst, err)
}
if err := copyRegularTree(child, sourcePath, dst, hooks); err != nil {
return err
}
if err := os.Chmod(dst, promotedDirectoryMode); err != nil {
return fmt.Errorf("set destination directory permissions %q: %w", dst, err)
}
if err := syncDirectory(dst); err != nil {
return fmt.Errorf("sync destination directory %q: %w", dst, err)
}
return nil
}
func copyRegularFile(
root *os.Root,
name, sourcePath, dst string,
inspected os.FileInfo,
) error {
in, err := root.Open(name)
if err != nil {
return fmt.Errorf("open source file %q: %w", sourcePath, err)
}
defer func() { _ = in.Close() }()
openedInfo, err := in.Stat()
opened, err := in.Stat()
if err != nil {
return fmt.Errorf("inspect opened source file %q: %w", src, err)
return fmt.Errorf("inspect opened source file %q: %w", sourcePath, err)
}
if !openedInfo.Mode().IsRegular() || !os.SameFile(inspected, openedInfo) {
return fmt.Errorf("source file %q changed while being copied", src)
if !opened.Mode().IsRegular() || !os.SameFile(inspected, opened) {
return fmt.Errorf("source file %q changed while being copied", sourcePath)
}
current, err := root.Lstat(name)
if err != nil {
return fmt.Errorf("reinspect source file %q: %w", sourcePath, err)
}
if current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() || !os.SameFile(opened, current) {
return fmt.Errorf("source file %q changed while being copied", sourcePath)
}
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, promotedFileMode)
@@ -166,7 +276,7 @@ func copyRegularFile(src, dst string, inspected os.FileInfo) error {
}()
if _, err := io.Copy(out, in); err != nil {
return fmt.Errorf("copy source file %q: %w", src, err)
return fmt.Errorf("copy source file %q: %w", sourcePath, err)
}
if err := out.Chmod(promotedFileMode); err != nil {
return fmt.Errorf("set destination file permissions %q: %w", dst, err)