v0.12.3 — Security & correctness bug fixes (33 bugs)

CRITICAL: 10 data race and security fixes — backup.go mutex coverage
(C1-C4), IsSystemDisk 12-bit major/minor (C5), /dev/ path validation
(C6), extractName traversal (C7), TargetPath/DestinationPath against
registered paths (C8-C9), ParseComposeHDDMounts Clean-before-prefix (C10).

HIGH: 17 logic/resource fixes — ValidateDump bufio.Scanner (H1), single
appDirSize() with 30s timeout (H2/H3), snapshot ID regex (H4), cross-drive
restic prune (H5), temp file order (H6), dirSizeBytes errors (H7), atomic
fstab (H8), IsDeviceMounted suffix check (H9), eMMC partition mapping (H10),
bytesCopied mutex (H11), separator-aware migrate prefix (H13), DeleteStack
error on compose-down (H14), docker 60s timeout (H16), NotificationPrefs
deep-copy (H17), wipefs warning (H18), fstab rollback on mount fail (H19).

MEDIUM: 7 code quality fixes — formatBytes dedup (M1), .tmp filter order
(M2), sizeBytes string type (M3), elapsed in message (M6), LoadLocation
fallback (M7), pathCovers separator (M10), cancelEditLabel textContent (M11).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 21:10:55 +01:00
parent 20b3a22c88
commit 93d9b474f1
17 changed files with 390 additions and 164 deletions
+38 -49
View File
@@ -1,6 +1,7 @@
package backup
import (
"bufio"
"context"
"fmt"
"log"
@@ -224,7 +225,7 @@ func DumpOne(ctx context.Context, db DiscoveredDB, dumpDir string, logger *log.L
result.Validation = ValidateDump(finalPath, db.DBType)
logger.Printf("[INFO] DB dump: %s → %s (%s, %s, %d tables)", db.ContainerName, filename,
formatBytes(stat.Size()), result.Duration.Round(time.Millisecond), result.Validation.TableCount)
humanizeBytes(stat.Size()), result.Duration.Round(time.Millisecond), result.Validation.TableCount)
return result
}
@@ -248,18 +249,45 @@ func ValidateDump(filePath string, dbType DBType) DumpValidation {
return v
}
data, err := os.ReadFile(filePath)
// H1: Use bufio.Scanner to read line-by-line instead of loading entire file into memory.
// Large dumps (500MB+) would cause massive allocations on every 5-min cache refresh.
f, err := os.Open(filePath)
if err != nil {
v.Error = fmt.Sprintf("read failed: %v", err)
log.Printf("[WARN] ValidateDump FAIL: %s — %s", filePath, v.Error)
return v
}
defer f.Close()
content := string(data)
scanner := bufio.NewScanner(f)
// Increase token buffer for very long lines (some SQL lines can be large)
scanner.Buffer(make([]byte, 256*1024), 256*1024)
// Count CREATE TABLE statements
lineNum := 0
headerFound := false
tableCount := 0
for _, line := range strings.Split(content, "\n") {
for scanner.Scan() {
line := scanner.Text()
lineNum++
// Header check — scan first 10 lines for expected dump header
// MariaDB 11.4+ prepends a sandbox comment before the header line
if lineNum <= 10 && !headerFound {
switch dbType {
case DBTypeMariaDB:
if strings.HasPrefix(line, "-- MariaDB dump") ||
strings.HasPrefix(line, "-- MySQL dump") ||
strings.HasPrefix(line, "-- mysqldump") {
headerFound = true
}
case DBTypePostgres:
if strings.HasPrefix(line, "-- PostgreSQL database dump") {
headerFound = true
}
}
}
// Count CREATE TABLE statements
upper := strings.ToUpper(strings.TrimSpace(line))
if strings.HasPrefix(upper, "CREATE TABLE") {
tableCount++
@@ -267,30 +295,6 @@ func ValidateDump(filePath string, dbType DBType) DumpValidation {
}
v.TableCount = tableCount
// Header check — scan first 10 lines for expected dump header
// MariaDB 11.4+ prepends a sandbox comment before the header line
headerFound := false
lines := strings.SplitN(content, "\n", 11) // at most 11 parts = 10 lines
for i, line := range lines {
if i >= 10 {
break
}
switch dbType {
case DBTypeMariaDB:
if strings.HasPrefix(line, "-- MariaDB dump") ||
strings.HasPrefix(line, "-- MySQL dump") ||
strings.HasPrefix(line, "-- mysqldump") {
headerFound = true
}
case DBTypePostgres:
if strings.HasPrefix(line, "-- PostgreSQL database dump") {
headerFound = true
}
}
if headerFound {
break
}
}
if !headerFound {
switch dbType {
case DBTypeMariaDB:
@@ -304,7 +308,7 @@ func ValidateDump(filePath string, dbType DBType) DumpValidation {
if tableCount == 0 {
v.Error = "no CREATE TABLE statements found"
log.Printf("[WARN] ValidateDump FAIL: %s — %s (header was found, scanned %d lines)", filePath, v.Error, len(strings.Split(content, "\n")))
log.Printf("[WARN] ValidateDump FAIL: %s — %s (header was found, scanned %d lines)", filePath, v.Error, lineNum)
return v
}
@@ -325,10 +329,11 @@ func ListDumpFiles(dumpDir string) ([]DumpFileInfo, error) {
var files []DumpFileInfo
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
// M2: Check .tmp before .sql to correctly skip ".sql.tmp" temp files (was dead code before).
if e.IsDir() || strings.HasSuffix(e.Name(), ".tmp") {
continue
}
if strings.HasSuffix(e.Name(), ".tmp") {
if !strings.HasSuffix(e.Name(), ".sql") {
continue
}
@@ -464,20 +469,4 @@ func cleanupTmpFiles(dumpDir string, logger *log.Logger) {
}
}
func formatBytes(b int64) string {
const (
kb = 1024
mb = 1024 * kb
gb = 1024 * mb
)
switch {
case b >= gb:
return fmt.Sprintf("%.1f GB", float64(b)/float64(gb))
case b >= mb:
return fmt.Sprintf("%.1f MB", float64(b)/float64(mb))
case b >= kb:
return fmt.Sprintf("%.1f KB", float64(b)/float64(kb))
default:
return fmt.Sprintf("%d B", b)
}
}
// M1: formatBytes removed — use humanizeBytes() from appdata.go (same package, no duplication).