Skip to content

runtime: zero cgroup CPU period yields NaN GOMAXPROCS and fatal "procresize: invalid arg" at startup #80783

Open

Description

@alex-fall-bindplane

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, nil

quota == 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 GOMAXPROCS to a positive integer, which makes schedinit take the env branch and never call defaultGOMAXPROCS.

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.

Activity

  1. alex-fall-bindplane commented on Aug 7, 2026

    @alex-fall-bindplane
    Author

    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:

    kernel=3.10.0-1160.el7.x86_64
    native quota=-1 period=100000
    --- baseline on native values
    OK built-with=go1.26.4 NumCPU=2 GOMAXPROCS=2
    spoofed quota=0 period=0
    --- with period spoofed to 0
    fatal error: procresize: invalid arg
    

    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 0 over them on a real cgroup v1 mount:

    mount -t cgroup -o cpu cgroup /cgroup/cpu
    printf '0\n' > /zeroval
    mount -o bind /zeroval /cgroup/cpu/cpu.cfs_quota_us
    mount -o bind /zeroval /cgroup/cpu/cpu.cfs_period_us
    ./hello    # GOOS=linux GOARCH=amd64 CGO_ENABLED=0, built with go1.26.4

    Nothing in ReadCPULimit or adjustCgroupGOMAXPROCS is 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 the int32 conversion, keeps that failure mode proportionate.

  2. seankhliao commented on Aug 7, 2026

    @seankhliao
    Member

    but the issue is still red hat back porting some patch that produces invalid values right?

  3. alex-fall-bindplane commented on Aug 7, 2026

    @alex-fall-bindplane
    Author

    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.

  4. added this to the Go1.28 milestone on Aug 11, 2026
  5. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Aug 11, 2026
  6. liwook commented on Aug 13, 2026

    @liwook

    Hi, I encountered a procresize: invalid arg panic 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)

    Item Value
    Go version go1.26.0 (toolchain v0.0.1-go1.26.0.linux-386)
    OS CentOS release 6.8 (Final)
    Kernel 2.6.32-696.3.2.el6.i686 #1 SMP Tue Jun 20 00:48:23 UTC 2017 i686 i386 GNU/Linux
    Architecture i686 (32-bit)
    Virtualization KVM (full virtualization, cloud server)
    CPU 2 cores (Intel Xeon Platinum 8163 CPU @ 2.50GHz)
    CPU Topology 1 socket, 1 core/socket, 2 threads/core (Hyper-Threading enabled)
    cgroup /sys/fs/cgroup/cpu/ does NOT exist

    Working Machine (Physical Machine - corget)

    Item Value
    Go version go1.26.0 (same binary)
    OS CentOS release 6.10 (Final)
    Kernel 2.6.32-754.35.1.el6.i686 #1 SMP Sat Nov 7 13:07:51 UTC 2020 i686 i386 GNU/Linux
    Architecture i686 (32-bit)
    Virtualization None (bare-metal / physical machine)
    CPU 4 cores (Intel Xeon E5-2407 CPU @ 2.20GHz)
    cgroup /sys/fs/cgroup/cpu/ does NOT exist

    Key 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 via sched_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:

    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("hello")
    }

    Compiled with GOOS=linux GOARCH=386 go build, it crashes immediately at startup with the same stack trace — before main() is even reached. This proves the bug is entirely in the runtime initialization, not in any application code.


    Full Stack Trace

    fatal error: procresize: invalid arg
    
    runtime stack:
    runtime.throw({0x81df096, 0x17})
    	/data/cuteFiles/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.0.linux-386/src/runtime/panic.go:1229 +0x35
    runtime.procresize(0x80000000)
    	/data/cuteFiles/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.0.linux-386/src/runtime/proc.go:6036 +0xb5b
    runtime.schedinit()
    	/data/cuteFiles/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.0.linux-386/src/runtime/proc.go:936 +0x41b
    runtime.rt0_go()
    	/data/cuteFiles/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.0.linux-386/src/runtime/asm_386.s:243 +0x161
    

    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:

    1. Running on linux/386 (32-bit)
    2. Running inside a KVM virtual machine
    3. Using an older 2.6.32-based kernel (confirmed on 2.6.32-696; not reproducible on 2.6.32-754 bare-metal)

    The runtime likely calls sched_getaffinity() or parses /proc/self/status Cpus_allowed field, and on this specific KVM + old kernel combination, the returned value causes a 32-bit overflow.


    Workaround

    Setting GOMAXPROCS to the actual core count completely bypasses the CPU detection logic:

    export GOMAXPROCS=2
    ./copynew

    The program then runs normally without any issues.


    Hope this detailed comparison helps pinpoint the root cause!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.compiler/runtimeIssues related to the Go compiler and/or runtime.

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions