Skip to content

Commit ae43188

Browse files
fix: validate filesystem capacity health probe
1 parent 14fdf39 commit ae43188

2 files changed

Lines changed: 71 additions & 5 deletions

File tree

‎cmd/golanggraph/health.go‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"net/http"
1414
"net/url"
1515
"os"
16+
"reflect"
1617
"strings"
1718
"syscall"
1819
"time"
@@ -220,19 +221,60 @@ func probeResources(opts healthOptions) []checkResult {
220221
if err := syscall.Statfs(wd, &stat); err != nil {
221222
results = append(results, checkResult{Name: "Disk space", Warning: true, Detail: "unavailable: " + err.Error()})
222223
} else {
223-
free := stat.Bavail * uint64(stat.Bsize)
224-
detail := fmt.Sprintf("%s free at %s", humanBytes(free), wd)
225-
if free < opts.MinFreeDiskBytes {
226-
results = append(results, checkResult{Name: "Disk space", Detail: detail + " (below the minimum)"})
224+
free, ok := availableDiskBytes(stat.Bavail, stat.Bsize)
225+
if !ok {
226+
results = append(results, checkResult{Name: "Disk space", Warning: true, Detail: "unavailable: invalid filesystem capacity"})
227227
} else {
228-
results = append(results, checkResult{Name: "Disk space", OK: true, Detail: detail})
228+
detail := fmt.Sprintf("%s free at %s", humanBytes(free), wd)
229+
if free < opts.MinFreeDiskBytes {
230+
results = append(results, checkResult{Name: "Disk space", Detail: detail + " (below the minimum)"})
231+
} else {
232+
results = append(results, checkResult{Name: "Disk space", OK: true, Detail: detail})
233+
}
229234
}
230235
}
231236

232237
results = append(results, probeMemory())
233238
return results
234239
}
235240

241+
// availableDiskBytes converts filesystem block counts to bytes without
242+
// accepting negative values or wrapping on multiplication.
243+
func availableDiskBytes(availableBlocks, blockSize any) (uint64, bool) {
244+
blocks, ok := filesystemCountToUint64(availableBlocks)
245+
if !ok {
246+
return 0, false
247+
}
248+
size, ok := filesystemCountToUint64(blockSize)
249+
if !ok || size == 0 {
250+
return 0, false
251+
}
252+
253+
if blocks != 0 && size > ^uint64(0)/blocks {
254+
return 0, false
255+
}
256+
return blocks * size, true
257+
}
258+
259+
// filesystemCountToUint64 normalizes the signed and unsigned statfs field
260+
// types used by supported operating systems. Signed values are validated
261+
// before conversion, so a malformed filesystem response cannot wrap.
262+
func filesystemCountToUint64(value any) (uint64, bool) {
263+
v := reflect.ValueOf(value)
264+
switch v.Kind() {
265+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
266+
signed := v.Int()
267+
if signed < 0 {
268+
return 0, false
269+
}
270+
return uint64(signed), true
271+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
272+
return v.Uint(), true
273+
default:
274+
return 0, false
275+
}
276+
}
277+
236278
// probeMemory reports available memory from the kernel where it is exposed.
237279
func probeMemory() checkResult {
238280
data, err := os.ReadFile("/proc/meminfo")

‎cmd/golanggraph/health_test.go‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,30 @@ func TestHealth_ResourceProbesAreMeasured(t *testing.T) {
153153
assert.True(t, sawFailure, "disk space must be measured, not assumed")
154154
}
155155

156+
func TestAvailableDiskBytes(t *testing.T) {
157+
tests := []struct {
158+
name string
159+
blocks int64
160+
size int64
161+
want uint64
162+
ok bool
163+
}{
164+
{name: "normal", blocks: 3, size: 4096, want: 12288, ok: true},
165+
{name: "zero blocks", blocks: 0, size: 4096, want: 0, ok: true},
166+
{name: "negative blocks", blocks: -1, size: 4096, ok: false},
167+
{name: "non-positive block size", blocks: 1, size: 0, ok: false},
168+
{name: "overflow", blocks: int64(^uint64(0) >> 1), size: 3, ok: false},
169+
}
170+
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
got, ok := availableDiskBytes(tt.blocks, tt.size)
174+
assert.Equal(t, tt.ok, ok)
175+
assert.Equal(t, tt.want, got)
176+
})
177+
}
178+
}
179+
156180
func itoa(n int) string {
157181
if n == 0 {
158182
return "0"

0 commit comments

Comments
 (0)