Repository navigation
runtime: zero cgroup CPU period yields NaN GOMAXPROCS and fatal "procresize: invalid arg" at startup #80783
Description
Go version
go1.25.7, go1.25.9, go1.26.4, and current master (the unguarded division is still present at master).
go1.23.8 and go1.24.2 are unaffected, since cgroup-aware GOMAXPROCS was added in Go 1.25.
Output of go env in your module/workspace
Cross-compiled GOOS=linux GOARCH=amd64 CGO_ENABLED=0 from a darwin/arm64 host. The defect is in the target runtime rather than the build host; a natively built linux/amd64 binary behaves the same.
What did you do?
Ran a trivial linux/amd64 Go binary on a RHEL/CentOS 6 host (kernel 2.6.32-754.el6.x86_64) with the cgroup v1 cpu controller mounted.
Red Hat backported CFS bandwidth control into their 2.6.32 kernel (CONFIG_CFS_BANDWIDTH=y), but the root cgroup reports:
cpu.cfs_quota_us = 0
cpu.cfs_period_us = 0
Upstream kernels and EL7+ report -1 / 100000 here, so this does not reproduce on a normal modern system.
The program is just:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("OK %s NumCPU=%d GOMAXPROCS=%d\n",
runtime.Version(), runtime.NumCPU(), runtime.GOMAXPROCS(0))
}What did you see happen?
It aborts before main():
fatal error: procresize: invalid arg
runtime stack:
runtime.throw({0x4c0bf2?, 0x0?})
/usr/local/go/src/runtime/panic.go:1094 +0x48 fp=0x7ffd1d631b40 sp=0x7ffd1d631b10 pc=0x46dd28
runtime.procresize(0x0?)
/usr/local/go/src/runtime/proc.go:5872 +0x9b6 fp=0x7ffd1d631c10 sp=0x7ffd1d631b40 pc=0x44a9b6
runtime.schedinit()
/usr/local/go/src/runtime/proc.go:935 +0x3b4 fp=0x7ffd1d631cb0 sp=0x7ffd1d631c10 pc=0x43f974
runtime.rt0_go()
/usr/local/go/src/runtime/asm_amd64.s:349 +0x11c fp=0x7ffd1d631cb8 sp=0x7ffd1d631cb0 pc=0x47209c
The same kernel with the cpu cgroup controller not mounted runs the binary fine, which is what isolates this to the cgroup path.
What did you expect to see?
The program to run. A zero (or otherwise unusable) period should be treated as "no limit", not turned into a negative GOMAXPROCS.
Analysis
src/internal/runtime/cgroup/cgroup_linux.go, ReadCPULimit, v1 branch:
if quota < 0 {
// No limit.
return 0, false, nil
}
period, err := readV1Number(c.periodFD)
if err != nil {
return 0, false, errMalformedFile
}
return float64(quota) / float64(period), true, nilquota == 0 is not < 0, so the no-limit early return is skipped, and there is no guard on period == 0. The division is 0/0, which is NaN.
That NaN then flows through adjustCgroupGOMAXPROCS in src/runtime/cgroup_linux.go:
limit = ceil(limit) // NaN
limit = max(limit, 2) // NaN — the builtin max propagates NaN
if int32(limit) < procs {
procs = int32(limit)
}Converting NaN to int32 on amd64 gives -2147483648, which compares less than the CPU count, so procs becomes negative. schedinit passes it to procresize, which throws on nprocs <= 0.
Measured on linux/amd64:
quota/period = NaN
ceil() = NaN
max(limit, 2) = NaN
int32(limit) = -2147483648
The cgroup v2 path in src/internal/runtime/cgroup/cgroup.go (parseV2Limit) ends in the same unguarded float64(quota) / float64(period), so a 0 period there would produce the same NaN. I have not observed a kernel writing that to cpu.max, but the guard is missing in both paths.
Suggested fix
Treat a non-positive period as "no limit" in both ReadCPULimit (v1) and parseV2Limit (v2), returning (0, false, nil). Defending adjustCgroupGOMAXPROCS against a non-finite limit would also stop any future parse bug from turning into a startup crash rather than a degraded GOMAXPROCS.
Workarounds
Either of these avoids the crash (both verified on go1.26.4 on the affected kernel):
GODEBUG=containermaxprocs=0— the NaN is still computed for the GODEBUG counter but is discarded, and the real CPU count is used.- Setting
GOMAXPROCSto a positive integer, which makesschedinittake the env branch and never calldefaultGOMAXPROCS.
Possibly related
#77374 is the same panic and stack trace on the same kernel version. It was closed by the reporter without a diagnosis, so the underlying cause was never recorded.
Reproduction environment
QEMU qemu-system-x86_64, the stock kernel-2.6.32-754.el6.x86_64 from vault.centos.org, and a busybox initramfs that mounts /proc, /sys, and mount -t cgroup -o cpu cgroup /cgroup/cpu. Happy to share the exact rig if useful.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
alex-fall-bindplane commentedon Aug 7, 2026
Follow-up to pre-empt the obvious triage question: the kernel version is not the variable here. I originally reported this on 2.6.32 because RHEL 6 presents these cgroup values natively, but that kernel is below the documented 3.2 floor and I did not want that to be mistaken for the cause.
Reproduced on
3.10.0-1160.el7.x86_64, comfortably above the minimum, with an A/B inside a single boot:Same binary, same kernel, same boot. The only change between the two runs is the contents of the limit files, which I substituted by bind-mounting a regular file containing
0over them on a real cgroup v1 mount:Nothing in
ReadCPULimitoradjustCgroupGOMAXPROCSis gated on kernel version, so any source of a zero period reaches the same unguarded division and the same fatal throw. RHEL 6 is one naturally occurring source; a nested or synthetic cgroupfs, or a future parse slip, would be others.The part I think is worth fixing regardless of which kernels are in scope: a degenerate CPU limit currently aborts the process before
main()rather than degrading GOMAXPROCS. A guard on a non-positive period, or a finite-value check before theint32conversion, keeps that failure mode proportionate.seankhliao commentedon Aug 7, 2026
but the issue is still red hat back porting some patch that produces invalid values right?
alex-fall-bindplane commentedon Aug 7, 2026
The reason I raise this is Go already decided bad cgroup data shouldn't be fatal. Unreadable or malformed limit files return errMalformedFile and the caller falls back to the CPU count. This skips that path. I just wanted to raise this since it was not very clear from the error what was wrong and it is surprising that this environment variable is able to avoid the panic.
gabyhelp commentedon Aug 7, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
liwook commentedon Aug 13, 2026
Hi, I encountered a
procresize: invalid argpanic on linux/386 (32-bit). I have a comparison with another 32-bit physical machine that works fine, which helps narrow down the trigger condition.Crashing Machine (KVM Virtual Machine - gps)
/sys/fs/cgroup/cpu/does NOT existWorking Machine (Physical Machine - corget)
/sys/fs/cgroup/cpu/does NOT existKey Finding
Both machines do NOT have
/sys/fs/cgroup/cpu/directory at all. So this is NOT a cgroup file parsing issue. The runtime must be determining CPU count viasched_getaffinity()or/proc/self/status(Cpus_allowed field).The crash only happens on the KVM virtual machine with an older kernel (2.6.32-696). The physical machine with a newer kernel (2.6.32-754) works fine.
Minimal Reproduction
Even a trivial program crashes on the KVM machine:
Compiled with
GOOS=linux GOARCH=386 go build, it crashes immediately at startup with the same stack trace — beforemain()is even reached. This proves the bug is entirely in the runtime initialization, not in any application code.Full Stack Trace
Root Cause Analysis
procresize(0x80000000)=2^31, which is out of range for a 32-bit signed int. This suggests an integer overflow or signed/unsigned conversion bug in the CPU detection logic on linux/386, triggered specifically when:The runtime likely calls
sched_getaffinity()or parses/proc/self/statusCpus_allowedfield, and on this specific KVM + old kernel combination, the returned value causes a 32-bit overflow.Workaround
Setting
GOMAXPROCSto the actual core count completely bypasses the CPU detection logic:export GOMAXPROCS=2 ./copynewThe program then runs normally without any issues.
Hope this detailed comparison helps pinpoint the root cause!