mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-19 08:47:46 +02:00
fix(agent): guard against unsigned underflow in memory calculations (closes #1978)
Extract host memory derivation into calculateHostMemoryUsage and use saturating subtraction to prevent uint64 underflow caused by race conditions when reading /proc/meminfo. Also ensure UsedPercent guards against division by zero. Co-authored-by: Sven van Ginkel <svenvanginkel@icloud.com>
This commit is contained in:
@@ -169,21 +169,11 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
|
||||
// memory
|
||||
if v, err := mem.VirtualMemory(); err == nil {
|
||||
used, cacheBuff, swapUsed := calculateHostMemoryUsage(v, a.memCalc == "htop")
|
||||
// swap
|
||||
systemStats.Swap = utils.BytesToGigabytes(v.SwapTotal)
|
||||
systemStats.SwapUsed = utils.BytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached)
|
||||
// cache + buffers value for default mem calculation
|
||||
// note: gopsutil automatically adds SReclaimable to v.Cached
|
||||
cacheBuff := v.Cached + v.Buffers - v.Shared
|
||||
if cacheBuff <= 0 {
|
||||
cacheBuff = max(v.Total-v.Free-v.Used, 0)
|
||||
}
|
||||
// htop memory calculation overrides (likely outdated as of mid 2025)
|
||||
if a.memCalc == "htop" {
|
||||
// cacheBuff = v.Cached + v.Buffers - v.Shared
|
||||
v.Used = v.Total - (v.Free + cacheBuff)
|
||||
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
||||
}
|
||||
systemStats.SwapUsed = utils.BytesToGigabytes(swapUsed)
|
||||
v.Used = used
|
||||
// if a.memCalc == "legacy" {
|
||||
// v.Used = v.Total - v.Free - v.Buffers - v.Cached
|
||||
// cacheBuff = v.Total - v.Free - v.Used
|
||||
@@ -193,10 +183,14 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
if a.zfs {
|
||||
if arcSize, _ := zfs.ARCSize(); arcSize > 0 && arcSize < v.Used {
|
||||
v.Used = v.Used - arcSize
|
||||
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
||||
systemStats.MemZfsArc = utils.BytesToGigabytes(arcSize)
|
||||
}
|
||||
}
|
||||
if v.Total > 0 {
|
||||
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
||||
} else {
|
||||
v.UsedPercent = 0
|
||||
}
|
||||
systemStats.Mem = utils.BytesToGigabytes(v.Total)
|
||||
systemStats.MemBuffCache = utils.BytesToGigabytes(cacheBuff)
|
||||
systemStats.MemUsed = utils.BytesToGigabytes(v.Used)
|
||||
@@ -263,6 +257,38 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
return systemStats
|
||||
}
|
||||
|
||||
// calculateHostMemoryUsage derives counters defensively because /proc/meminfo may
|
||||
// change while gopsutil reads it. Invalid unsigned subtractions saturate at zero.
|
||||
func calculateHostMemoryUsage(v *mem.VirtualMemoryStat, htop bool) (used, cacheBuff, swapUsed uint64) {
|
||||
used = v.Used
|
||||
if used > v.Total {
|
||||
used = saturatingSub(v.Total, v.Available)
|
||||
}
|
||||
|
||||
// gopsutil automatically adds SReclaimable to Cached.
|
||||
cacheBuff = min(v.Cached, v.Total)
|
||||
cacheBuff += min(v.Buffers, v.Total-cacheBuff)
|
||||
cacheBuff = saturatingSub(cacheBuff, min(v.Shared, v.Total))
|
||||
if v.Cached == 0 && v.Buffers == 0 {
|
||||
cacheBuff = saturatingSub(v.Total, v.Free, used)
|
||||
}
|
||||
if htop {
|
||||
used = saturatingSub(v.Total, v.Free, cacheBuff)
|
||||
}
|
||||
return used, cacheBuff, saturatingSub(v.SwapTotal, v.SwapFree, v.SwapCached)
|
||||
}
|
||||
|
||||
// saturatingSub subtracts each value, returning zero on underflow.
|
||||
func saturatingSub(value uint64, subtrahends ...uint64) uint64 {
|
||||
for _, subtrahend := range subtrahends {
|
||||
if subtrahend > value {
|
||||
return 0
|
||||
}
|
||||
value -= subtrahend
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
|
||||
func getOsPrettyName() (string, error) {
|
||||
file, err := os.Open("/etc/os-release")
|
||||
|
||||
Reference in New Issue
Block a user