diff --git a/controller/internal/storage/attach_linux.go b/controller/internal/storage/attach_linux.go index 786da65..16a5e87 100644 --- a/controller/internal/storage/attach_linux.go +++ b/controller/internal/storage/attach_linux.go @@ -359,15 +359,7 @@ func appendBindFstabEntry(fstabPath, source, target string) error { entry := fmt.Sprintf("\n# Bind mount (auto-generated by felhom-controller)\n%s\t%s\tnone\tbind,nofail\t0 0\n", source, target) newContent := append(existing, []byte(entry)...) - tmpPath := fstabPath + ".tmp" - if err := os.WriteFile(tmpPath, newContent, 0644); err != nil { - return fmt.Errorf("cannot write fstab tmp file: %w", err) - } - if err := os.Rename(tmpPath, fstabPath); err != nil { - os.Remove(tmpPath) - return fmt.Errorf("cannot rename fstab tmp file: %w", err) - } - return nil + return safeWriteFile(fstabPath, newContent, 0644) } // removeBindFstabEntry removes the bind mount fstab entry for the given target mount path. @@ -395,14 +387,5 @@ func removeBindFstabEntry(fstabPath, targetMountPath string) error { kept = append(kept, line) } - newContent := strings.Join(kept, "\n") - tmpPath := fstabPath + ".tmp" - if err := os.WriteFile(tmpPath, []byte(newContent), 0644); err != nil { - return fmt.Errorf("cannot write fstab tmp file: %w", err) - } - if err := os.Rename(tmpPath, fstabPath); err != nil { - os.Remove(tmpPath) - return fmt.Errorf("cannot rename fstab tmp file: %w", err) - } - return nil + return safeWriteFile(fstabPath, []byte(strings.Join(kept, "\n")), 0644) } diff --git a/controller/internal/storage/safety_linux.go b/controller/internal/storage/safety_linux.go index 8616d25..fc71f4e 100644 --- a/controller/internal/storage/safety_linux.go +++ b/controller/internal/storage/safety_linux.go @@ -105,10 +105,10 @@ func BackupFstab(fstabPath string) error { return os.WriteFile(backupPath, data, 0644) } -// AppendFstabEntry appends a UUID-based fstab entry atomically (write tmp + rename). -// H8: Direct write to /etc/fstab risks corruption on crash — use atomic write pattern. +// AppendFstabEntry appends a UUID-based fstab entry. +// Uses atomic rename when possible, falls back to direct overwrite for bind-mounted files +// (Docker mounts /etc/fstab as /host-fstab — rename fails with EBUSY on bind mounts). func AppendFstabEntry(fstabPath, uuid, mountPoint, fsType, options string) error { - // Read existing content existing, err := os.ReadFile(fstabPath) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("cannot read fstab: %w", err) @@ -117,19 +117,10 @@ func AppendFstabEntry(fstabPath, uuid, mountPoint, fsType, options string) error entry := fmt.Sprintf("\nUUID=%s\t%s\t%s\t%s\t0 2\n", uuid, mountPoint, fsType, options) newContent := append(existing, []byte(entry)...) - // Write to .tmp then rename — atomic on same filesystem - tmpPath := fstabPath + ".tmp" - if err := os.WriteFile(tmpPath, newContent, 0644); err != nil { - return fmt.Errorf("cannot write fstab tmp file: %w", err) - } - if err := os.Rename(tmpPath, fstabPath); err != nil { - os.Remove(tmpPath) - return fmt.Errorf("cannot rename fstab tmp file: %w", err) - } - return nil + return safeWriteFile(fstabPath, newContent, 0644) } -// RemoveFstabEntry removes any line containing the given UUID from fstab, atomically. +// RemoveFstabEntry removes any line containing the given UUID from fstab. // H19: Called as rollback if mount fails after fstab was written. func RemoveFstabEntry(fstabPath, uuid string) error { data, err := os.ReadFile(fstabPath) @@ -145,13 +136,24 @@ func RemoveFstabEntry(fstabPath, uuid string) error { } newContent := strings.Join(kept, "\n") - tmpPath := fstabPath + ".tmp" - if err := os.WriteFile(tmpPath, []byte(newContent), 0644); err != nil { - return fmt.Errorf("cannot write fstab tmp file: %w", err) + return safeWriteFile(fstabPath, []byte(newContent), 0644) +} + +// safeWriteFile writes content to a file. It tries atomic rename first (write to .tmp, +// then rename). If rename fails (e.g., bind-mounted files where rename returns EBUSY), +// it falls back to a direct truncate-and-write to the target file. +func safeWriteFile(path string, content []byte, perm os.FileMode) error { + tmpPath := path + ".tmp" + if err := os.WriteFile(tmpPath, content, perm); err != nil { + return fmt.Errorf("cannot write tmp file %s: %w", tmpPath, err) } - if err := os.Rename(tmpPath, fstabPath); err != nil { - os.Remove(tmpPath) - return fmt.Errorf("cannot rename fstab tmp file: %w", err) + if err := os.Rename(tmpPath, path); err == nil { + return nil // atomic rename succeeded + } + // Rename failed (likely bind mount) — fall back to direct write + os.Remove(tmpPath) + if err := os.WriteFile(path, content, perm); err != nil { + return fmt.Errorf("cannot write %s: %w", path, err) } return nil }