diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 01450aad8..7020e83c4 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -291,6 +291,11 @@ canonical physical-disk model and the Proxmox polling runtime in `internal/monitoring/monitor_pve.go` must evaluate disk alerts only after that merged disk view exists, so controller-backed disks do not lose health and endurance coverage between collection and alerting. +That same Proxmox monitoring boundary also owns checked response parsing for +polymorphic numeric fields. Shared client parsers such as +`pkg/proxmox/replication.go` must use the package's checked integer conversion +helpers instead of direct casts, so malformed or oversized Proxmox values do +not overflow into monitoring state. Backup polling and recovery guest identity assembly now derive workload node, name, and type context from canonical `ReadState` instead of from diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index 8d2df205a..f58458593 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -126,6 +126,13 @@ func floatToIntTrunc(v float64) (int, error) { return int(v), nil } +func floatToIntRounded(v float64) (int, error) { + if math.IsNaN(v) || math.IsInf(v, 0) { + return 0, fmt.Errorf("non-finite float %v cannot be converted to int", v) + } + return floatToIntTrunc(math.Round(v)) +} + func looksLikeNumericLiteral(raw string) bool { hasDigit := false var prev rune diff --git a/pkg/proxmox/replication.go b/pkg/proxmox/replication.go index 919b6e1fd..f5e3babca 100644 --- a/pkg/proxmox/replication.go +++ b/pkg/proxmox/replication.go @@ -368,33 +368,39 @@ func intFromAny(value interface{}) (int, bool) { case int32: return int(v), true case int64: - return int(v), true + parsed, err := int64ToInt(v) + return parsed, err == nil case uint: - return int(v), true + parsed, err := uint64ToInt(uint64(v)) + return parsed, err == nil case uint8: return int(v), true case uint16: return int(v), true case uint32: - return int(v), true + parsed, err := uint64ToInt(uint64(v)) + return parsed, err == nil case uint64: - return int(v), true + parsed, err := uint64ToInt(v) + return parsed, err == nil case float32: - if math.IsNaN(float64(v)) || math.IsInf(float64(v), 0) { - return 0, false - } - return int(math.Round(float64(v))), true + parsed, err := floatToIntRounded(float64(v)) + return parsed, err == nil case float64: - if math.IsNaN(v) || math.IsInf(v, 0) { + parsed, err := floatToIntRounded(v) + return parsed, err == nil + case json.Number: + s := strings.TrimSpace(v.String()) + if i, err := v.Int64(); err == nil { + parsed, err := int64ToInt(i) + return parsed, err == nil + } + if !looksLikeFloatLiteral(s) { return 0, false } - return int(math.Round(v)), true - case json.Number: - if i, err := v.Int64(); err == nil { - return int(i), true - } - if f, err := v.Float64(); err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) { - return int(math.Round(f)), true + if f, err := v.Float64(); err == nil { + parsed, err := floatToIntRounded(f) + return parsed, err == nil } case string: s := strings.TrimSpace(v) @@ -402,15 +408,24 @@ func intFromAny(value interface{}) (int, bool) { return 0, false } if i, err := strconv.ParseInt(s, 10, 64); err == nil { - return int(i), true + parsed, err := int64ToInt(i) + return parsed, err == nil } - if f, err := strconv.ParseFloat(s, 64); err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) { - return int(math.Round(f)), true + if !looksLikeFloatLiteral(s) { + return 0, false + } + if f, err := strconv.ParseFloat(s, 64); err == nil { + parsed, err := floatToIntRounded(f) + return parsed, err == nil } } return 0, false } +func looksLikeFloatLiteral(s string) bool { + return strings.ContainsAny(s, ".eE") +} + func boolFromAny(value interface{}) (bool, bool) { switch v := value.(type) { case nil: diff --git a/pkg/proxmox/replication_test.go b/pkg/proxmox/replication_test.go index ec46f50c7..a4d570909 100644 --- a/pkg/proxmox/replication_test.go +++ b/pkg/proxmox/replication_test.go @@ -94,6 +94,7 @@ func TestIntFromAny(t *testing.T) { {"uint16", uint16(65535), 65535, true}, {"uint32", uint32(42), 42, true}, {"uint64", uint64(42), 42, true}, + {"uint64 overflow", ^uint64(0), 0, false}, // float types (rounded) {"float32 integer", float32(42.0), 42, true}, @@ -107,16 +108,21 @@ func TestIntFromAny(t *testing.T) { {"float64 NaN", math.NaN(), 0, false}, {"float64 +Inf", math.Inf(1), 0, false}, {"float64 -Inf", math.Inf(-1), 0, false}, + {"float64 overflow", math.MaxFloat64, 0, false}, // json.Number {"json.Number int", json.Number("42"), 42, true}, {"json.Number float", json.Number("42.6"), 43, true}, + {"json.Number scientific float", json.Number("4.2e1"), 42, true}, + {"json.Number oversized int", json.Number("9223372036854775808"), 0, false}, {"json.Number invalid", json.Number("abc"), 0, false}, // string {"string int", "42", 42, true}, {"string negative", "-42", -42, true}, {"string float", "42.6", 43, true}, + {"string scientific float", "4.2e1", 42, true}, + {"string oversized int", "9223372036854775808", 0, false}, {"string empty", "", 0, false}, {"string whitespace", " 42 ", 42, true}, {"string invalid", "abc", 0, false},