Memory safety: data race leading to nil pointer dereference (use-after-init) in Go
Description
The commit fixes a data race and potential nil pointer dereference in the storage startup sequence. Previously, FreeDiskSpaceWatcher could start before the Storage.table was fully initialized (opened), leading to the watcher potentially reading an uninitialized or partially initialized table and dereferencing a nil or invalid pointer. The fix initializes the isReadOnly state earlier and starts FreeDiskSpaceWatcher after openTable, ensuring the shared storage state is fully initialized before the watcher accesses it. This mitigates a memory safety issue (use-after-init / nil pointer dereference) that could cause crashes or undefined behavior in concurrent operation.
Proof of Concept
package main
import (
"fmt"
"time"
)
type Table struct{ val int }
func (t *Table) SomeMethod() int {
// dereference to simulate potential nil dereference if t is nil
return t.val + 1
}
type Storage struct {
table *Table
}
func (s *Storage) startFreeDiskSpaceWatcher() {
go func() {
for {
// This line will panic if s.table is nil
_ = s.table.SomeMethod()
time.Sleep(1 * time.Millisecond)
}
}()
}
func main() {
s := &Storage{}
s.startFreeDiskSpaceWatcher()
// Delay to create a race window before the table is allocated
time.Sleep(2 * time.Millisecond)
s.table = &Table{val: 0}
time.Sleep(100 * time.Millisecond)
fmt.Println("Done")
}
Commit Details
Author: Noureldin
Date: 2026-04-10 06:33 UTC
Message:
lib/storage: fixes data race at startFreeDiskSpaceWatcher
Previously, Storage.table was initialized after startFreeDiskSpaceWatcher was called.
This created a potential data race condition: if openTable took a long time to complete
and freed disk space during that window, the free disk space watcher could read an
uninitialized (or partially initialized) Storage.table, leading to an invalid memory
address or nil pointer dereference panic.
This commit properly initializes s.isReadOnly state during storage start and
starts FreeDiskSpaceWatcher after openTable.
Bug was introduced in github.com/VictoriaMetrics/VictoriaMetrics/commit/27b958ba8bc66578206ddac26ccf47b2cc3e8101
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10747
Triage Assessment
Vulnerability Type: Memory safety (data race / nil pointer dereference)
Confidence: HIGH
Reasoning:
The commit fixes a data race and potential nil pointer dereference by ensuring proper initialization order of storage state and starting the FreeDiskSpaceWatcher after the table is opened. This mitigates a memory safety issue (use-after-free-/nil dereference) that could lead to crashes or undefined behavior, which in practice is a security-relevant vulnerability in concurrent code.
Verification Assessment
Vulnerability Type: Memory safety: data race leading to nil pointer dereference (use-after-init) in Go
Confidence: HIGH
Affected Versions: Versions prior to 1.139.0 (pre-fix), e.g. 1.138.x and older
Code Diff
diff --git a/lib/storage/storage.go b/lib/storage/storage.go
index d74d6a6e77729..3b9ecda7ae398 100644
--- a/lib/storage/storage.go
+++ b/lib/storage/storage.go
@@ -268,7 +268,8 @@ func MustOpenStorage(path string, opts OpenOptions) *Storage {
// check for free disk space before opening the table
// to prevent unexpected part merges. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4023
- s.startFreeDiskSpaceWatcher()
+ freeSpaceBytes := fs.MustGetFreeSpace(s.path)
+ s.isReadOnly.Store(freeSpaceBytes < freeDiskSpaceLimitBytes)
// Load data
tablePath := filepath.Join(path, dataDirname)
@@ -314,6 +315,7 @@ func MustOpenStorage(path string, opts OpenOptions) *Storage {
s.startCurrHourMetricIDsUpdater()
s.startNextDayMetricIDsUpdater()
s.startLegacyRetentionWatcher()
+ s.startFreeDiskSpaceWatcher()
return s
}