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:
@@ -179,7 +179,7 @@ func (m *Manager) RunDBDumps(ctx context.Context) error {
|
||||
m.logger.Printf("[ERROR] DB dump failed for %s: %v", r.DB.ContainerName, r.Error)
|
||||
} else {
|
||||
totalSize += r.Size
|
||||
summary = append(summary, fmt.Sprintf("OK %s (%s)", r.DB.ContainerName, formatBytes(r.Size)))
|
||||
summary = append(summary, fmt.Sprintf("OK %s (%s)", r.DB.ContainerName, humanizeBytes(r.Size)))
|
||||
|
||||
// Persist validation result to settings.json
|
||||
if m.settings != nil && r.FilePath != "" {
|
||||
@@ -212,12 +212,12 @@ func (m *Manager) RunDBDumps(ctx context.Context) error {
|
||||
// Ping healthcheck
|
||||
uuid := m.cfg.Monitoring.PingUUIDs.DBDump
|
||||
body := fmt.Sprintf("DB dump: %d databases, %s total\n%s",
|
||||
len(results), formatBytes(totalSize), strings.Join(summary, "\n"))
|
||||
len(results), humanizeBytes(totalSize), strings.Join(summary, "\n"))
|
||||
|
||||
if allOK {
|
||||
m.pinger.Ping(uuid, body)
|
||||
m.logger.Printf("[INFO] DB dump completed: %d databases, %s total (%s)",
|
||||
len(results), formatBytes(totalSize), duration.Round(time.Millisecond))
|
||||
len(results), humanizeBytes(totalSize), duration.Round(time.Millisecond))
|
||||
} else {
|
||||
m.pinger.Fail(uuid, body)
|
||||
return fmt.Errorf("some database dumps failed")
|
||||
@@ -410,8 +410,11 @@ func (m *Manager) ListSnapshots(limit int) ([]SnapshotInfo, error) {
|
||||
}
|
||||
|
||||
// SetStackProvider sets the stack data provider for app data discovery.
|
||||
// C3: Write is protected by mutex since stackProvider is read by concurrent goroutines.
|
||||
func (m *Manager) SetStackProvider(provider StackDataProvider) {
|
||||
m.mu.Lock()
|
||||
m.stackProvider = provider
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetStackHDDMounts returns HDD mount paths for the named stack via the stack provider.
|
||||
@@ -551,8 +554,19 @@ func (m *Manager) RefreshCache(nextDBDump, nextBackup time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-check: if LastDBDump results have empty validation but files exist,
|
||||
// re-validate from disk. This handles controller restarts and race conditions.
|
||||
// Fill in dynamic fields under lock.
|
||||
// C1: lastDBDump mutation also happens here to prevent data races with GetFullStatus.
|
||||
// C2: snapshot history reversal happens before cachedStatus assignment (inside lock).
|
||||
m.mu.Lock()
|
||||
status.Running = m.running
|
||||
status.LastDBDump = m.lastDBDump
|
||||
status.LastBackup = m.lastBackup
|
||||
status.LastCheckTime = m.lastCheckTime
|
||||
status.LastCheckOK = m.lastCheckOK
|
||||
status.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory))
|
||||
copy(status.SnapshotHistory, m.snapshotHistory)
|
||||
|
||||
// C1: Cross-check lastDBDump results inside lock to prevent torn writes.
|
||||
if m.lastDBDump != nil && filesErr == nil {
|
||||
fileValidation := make(map[string]DumpValidation) // keyed by filename
|
||||
for _, f := range files {
|
||||
@@ -570,24 +584,15 @@ func (m *Manager) RefreshCache(nextDBDump, nextBackup time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in dynamic fields under lock
|
||||
m.mu.Lock()
|
||||
status.Running = m.running
|
||||
status.LastDBDump = m.lastDBDump
|
||||
status.LastBackup = m.lastBackup
|
||||
status.LastCheckTime = m.lastCheckTime
|
||||
status.LastCheckOK = m.lastCheckOK
|
||||
status.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory))
|
||||
copy(status.SnapshotHistory, m.snapshotHistory)
|
||||
m.cachedStatus = status
|
||||
m.cacheTime = time.Now()
|
||||
m.mu.Unlock()
|
||||
|
||||
// Reverse so newest first
|
||||
// C2: Reverse snapshot history before assigning to cachedStatus (inside lock).
|
||||
for i, j := 0, len(status.SnapshotHistory)-1; i < j; i, j = i+1, j-1 {
|
||||
status.SnapshotHistory[i], status.SnapshotHistory[j] = status.SnapshotHistory[j], status.SnapshotHistory[i]
|
||||
}
|
||||
|
||||
m.cachedStatus = status
|
||||
m.cacheTime = time.Now()
|
||||
m.mu.Unlock()
|
||||
|
||||
m.logger.Printf("[INFO] Backup status cache refreshed")
|
||||
}
|
||||
|
||||
@@ -616,8 +621,19 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta
|
||||
status.Running = m.running
|
||||
status.NextDBDump = nextDBDump
|
||||
status.NextBackup = nextBackup
|
||||
status.LastDBDump = m.lastDBDump
|
||||
status.LastBackup = m.lastBackup
|
||||
// C4: Deep-copy lastDBDump and lastBackup so callers cannot mutate shared state.
|
||||
if m.lastDBDump != nil {
|
||||
copyDump := *m.lastDBDump
|
||||
if len(m.lastDBDump.Results) > 0 {
|
||||
copyDump.Results = make([]DumpResult, len(m.lastDBDump.Results))
|
||||
copy(copyDump.Results, m.lastDBDump.Results)
|
||||
}
|
||||
status.LastDBDump = ©Dump
|
||||
}
|
||||
if m.lastBackup != nil {
|
||||
copyBackup := *m.lastBackup
|
||||
status.LastBackup = ©Backup
|
||||
}
|
||||
// Update snapshot history
|
||||
status.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory))
|
||||
copy(status.SnapshotHistory, m.snapshotHistory)
|
||||
|
||||
Reference in New Issue
Block a user