Repository navigation
Description
Description
On a VPS running under legacy OpenVZ virtualization, a self-contained .NET application fails to start after updating to .NET Runtime 10.0.12, with:
GC: This process is affinitize to 0 CPUs, check your GC heap affinity related configs
GC heap initialization failed with error 0x8013200A
Failed to create CoreCLR, HRESULT: 0x8013200A
The app previously started fine on an earlier 10.0.11 runtime.
Root cause appears to be that the CPU-count/affinity detection logic combines two data sources that are inconsistent under this legacy OpenVZ configuration, and the Server GC init path ends up computing an effective CPU count of 0, while the Workstation GC path does not hit this and starts normally.
Environment
- .NET Runtime: 10.0.12 (self-contained deployment)
- Target Framework: net10.0
- OS: Linux (guest), virtualization detected as
openvzviasystemd-detect-virt - Deployment type: self-contained executable (no
dotnetCLI/SDK installed on the host)
Diagnostic data collected
== nproc --all ==
4
== /proc/self/status | grep Cpus_allowed ==
Cpus_allowed: ffff,ffffffff,ffffffff,ffffffff
Cpus_allowed_list: 0-111
== systemd-detect-virt ==
openvz
== stat -fc %T /sys/fs/cgroup/ ==
tmpfs
== cgroup v2 cpuset.cpus.effective ==
(empty)
== cgroup v1 cpuset/cpuset.cpus ==
0-111
== GC-related environment variables at time of failure ==
(none set)
Key observation: nproc correctly reports the VPS's actual CPU quota (4), but the process's reported affinity mask (/proc/self/status) and the cgroup cpuset both report the full host range (0-111). /sys/fs/cgroup is mounted as a plain tmpfs, not a real cgroupfs, which is typical of legacy (non-namespace-aware) OpenVZ containers — the cgroup files exist for compatibility but don't reflect real per-container limits.
Repro steps
- Deploy a self-contained net10.0 app on a legacy OpenVZ VPS where:
nprocreports the real CPU quota (e.g. 4), but/proc/self/statusCpus_allowedreports the full host CPU range, and/sys/fs/cgroupis atmpfsstub (not real cgroupfs), withcpuset.cpus/cpuset.cpus.effectivealso reporting the host range or being empty.
- Run the app with default settings (Server GC enabled, no affinity overrides).
- Observe immediate crash on startup with HRESULT
0x8013200A.
What we tried
| Setting | Result |
|---|---|
| Default (Server GC, no overrides) | Fails — 0x8013200A |
DOTNET_PROCESSOR_COUNT=4 + DOTNET_GCHeapCount=4 + DOTNET_GCNoAffinitize=1 (Server GC still on) |
Fails — same error |
DOTNET_gcServer=0 + DOTNET_GCNoAffinitize=1 (Workstation GC) |
Works |
This suggests the issue is specific to the Server GC initialization/affinitization path, not general CPU-count detection — Workstation GC starts fine on the same host with the same conflicting /proc/cgroup data.
Expected behavior
The runtime should either:
- correctly reconcile the CPU count from the available signals (e.g. prefer whatever mechanism
nproc/the actual quota uses) instead of computing an intersection that yields 0, or - fail gracefully / fall back to a sane non-zero heap/thread count instead of a hard abort, particularly since Workstation GC succeeds on the identical host.
Additional notes
- This is very likely a niche environment (legacy, non-namespace-aware OpenVZ hosting is uncommon today), so reproducing it outside such a host may be difficult. Happy to gather more diagnostics on request.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Sep 8, 2026
Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.
lg2de commentedon Sep 9, 2026
Root cause found: regression from #131740 (backport of #126763)
I bisected this against the release tags. Between
v10.0.11andv10.0.12there are 29 commits, and exactly one touches CPU/affinity code:That PR backported #126763 + #127572 into
release/10.0on 2026-08-07, which is why 10.0.11 works and 10.0.12 does not.The mechanism
The relevant part of the diff in
src/coreclr/gc/unix/gcenv.unix.cpp:Before, the mask was a fixed
cpu_set_t— always 128 bytes / 1024 bits. Now it is sized fromminipal_get_cpu_max_possible_count(), which reads/sys/devices/system/cpu/possible.The kernel rejects a mask buffer that is narrower than its own
nr_cpu_ids:On this host, sysfs is virtualized per container but the syscall is not, so those two numbers disagree:
/sys/devices/system/cpu/possible0-3→minipal_get_cpu_max_possible_count()= 4CPU_ALLOC_SIZE(4)sizeof(unsigned long))/proc/self/status→Cpus_allowed_list0-111→ kernelnr_cpu_ids= 11264 < 112→ EINVALAnd the error is swallowed in release builds (
gcenv.unix.cpp#L208-L213):So
g_processAffinitySetstays empty, andinterface.cpp#L265-L269turns that intoCLR_E_GC_BAD_AFFINITY_CONFIG=0x8013200A.Additional diagnostics from the affected host
The host kernel is built with
NR_CPUS=5120and runs withnr_cpu_ids=112, while the container's sysfs consistently reports 4 CPUs.Correcting my own original report: cgroups are not involved at all.
/sysitself is a real sysfs and/sys/devices/system/cpu/possibleexists and parses fine — it just reports the container's view. Thetmpfs/sys/fs/cgroupI mentioned is a red herring;minipal_get_cpu_max_possible_count()never looks at cgroups.Why only Server GC is affected
The same PR added a graceful fallback everywhere except in the GC:
sched_getaffinityfailurepal/src/misc/sysinfo.cppif (nrcpus < 1) nrcpus = PAL_GetTotalCpuCount();nativeaot/Runtime/unix/PalUnix.cppif (count == 0) count = GetTotalProcessorCount();pal/src/thread/thread.cppgc/unix/gcenv.unix.cppassert(false)— set stays emptyWorkstation GC never reaches the affinity check, which is exactly why it starts fine on the identical host.
Why none of the environment variables helped
The
IsEmpty()check atinterface.cpp:265runs before everything else:DOTNET_PROCESSOR_COUNT/DOTNET_GCHeapCountare only consulted from line 280 onwards.DOTNET_GCNoAffinitizeis only read at line 298.GCHeapAffinitizeRangescannot help either:SetGCThreadsAffinitySet()only removes CPUs fromg_processAffinitySet, it never adds any — an empty set can never become non-empty.Worth noting that the code just below already tolerates an empty set gracefully (
interface.cpp#L302-L307explicitly guardsnum_affinitized_processors != 0). The hard failure at line 265 is the only thing standing in the way.Proposed fix
Give the GC path the same fallback the sibling files already got in that PR:
This is a no-op on healthy systems (the branch is never taken there) and yields 4 heaps on 4 CPUs here. It touches no
gc_heapcode, so per garbage-collector-guidelines.md I do not believe a stress run is required — please correct me if you see it differently.Two follow-on paths I checked, both harmless:
SetThreadAffinity()allocates an equally narrow mask, butsched_setaffinitytolerates a short buffer (get_user_cpu_maskclears the mask and copies onlylenbytes), and even on failureset_thread_affinity_for_heap(gc.cpp:2506-2512) only emits adprintf.Separately, and probably as its own PR: the genuinely correct fix is to retry
sched_getaffinitywith a doubled buffer whileerrno == EINVAL, which is the pattern glibc documents forCPU_ALLOC. That would handle any case where the kernel's mask is wider than sysfs suggests, not just this one. I kept it out of the fix above to keep the change backportable.I have the change ready and am happy to open a PR against
main(with a/backport to release/10.0afterwards) if that sounds right to you. Caveat: I can only reason about it statically — I have no way to build a runtime and validate it on the affected host, and the branch is unreachable on normal hardware, so I could not exercise it locally.Fix cpu affinity set size to have minimal size
antonlindgren commentedon Sep 9, 2026
Hi!
What a pleasant report to read, kudos @lg2de :)
We're seeing the same issue on our self-hosted github runners. It worked yesterday with
10.0.11but not today with10.0.12, and setting bothDOTNET_gcServer=0andDOTNET_GCNoAffinitize=1also did the trick. Thanks!Though, and excuse me if this is a dumb question, I'm wondering about the milestone flag - does that mean dotnet10 will not be getting this fix, @janvorli?
Thanks in advance!
Fix cpu affinity set size to have minimal size (#133497)