Skip to content

Fix runtime initialization on linux with cpu hotplug enabled - #128069

Merged
janvorli merged 2 commits into
dotnet:mainfrom
BrzVlad:fix-affinity
May 12, 2026
Merged

Fix runtime initialization on linux with cpu hotplug enabled#128069
janvorli merged 2 commits into
dotnet:mainfrom
BrzVlad:fix-affinity

Conversation

@BrzVlad

@BrzVlad BrzVlad commented May 12, 2026

Copy link
Copy Markdown
Member

On unix, during initialization, the runtime obtains the total number of CPUs via sysconf(_SC_NPROCESSORS_CONF). This should return the current number of cpus that are currently present on the system. It turns out linux has cpu hotplug support, so this number can increase. When hotplug is enabled, the kernel reserves storage for the max possible number of CPUs. This max number is exported in /sys/devices/system/cpu/possible. The problem is that, when allocating the cpu_set_t* for use with sched_getaffinity, this api failed because the OS expected for the cpu set to have reserved space for the maximum amount of cpu's, not just for the ones that are currently present.

Copilot AI review requested due to automatic review settings May 12, 2026 10:11
@BrzVlad
BrzVlad requested a review from MichalStrehovsky as a code owner May 12, 2026 10:11
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a shared native helper to determine an upper bound for CPU indices on Unix (especially Linux with CPU hotplug) and uses it to size cpu_set_t allocations for sched_getaffinity/sched_setaffinity, avoiding initialization failures due to undersized affinity masks.

Changes:

  • Added minipal_get_cpu_max_possible_count() in minipal to provide a “max possible CPU count” for CPU_ALLOC sizing.
  • Updated CoreCLR PAL, GC Unix init, and NativeAOT Unix PAL to use the new helper instead of calling sysconf(_SC_NPROCESSORS_CONF) directly.
  • Wired the new minipal source into the minipal CMake build on Unix.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/native/minipal/cpucount.h Declares new minipal CPU max-possible count helper.
src/native/minipal/cpucount.c Implements Linux sysfs-based lookup with sysconf fallback.
src/native/minipal/CMakeLists.txt Adds cpucount.c to minipal sources on Unix builds.
src/coreclr/pal/src/thread/thread.cpp Uses helper to size affinity mask when resetting thread affinity.
src/coreclr/pal/src/misc/sysinfo.cpp Uses helper when sizing affinity mask for logical CPU count detection.
src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp Uses helper when sizing affinity mask for NativeAOT CPU count initialization.
src/coreclr/gc/unix/gcenv.unix.cpp Uses helper for affinity set sizing during GC Unix initialization.
Comments suppressed due to low confidence (1)

src/coreclr/pal/src/thread/thread.cpp:1405

  • minipal_get_cpu_max_possible_count() is called on every thread start in ThreadEntry, and on Linux it currently opens/parses a sysfs file each time. Thread creation can be frequent (threadpool, timers), so this adds avoidable syscalls/I/O to a hot-ish path. Consider caching the computed max-possible CPU count (e.g., a static cached value in minipal with a benign race, or pthread_once) so subsequent calls are just a load.
            // In the unlikely event that minipal_get_cpu_max_possible_count() fails, just assume a reasonable default maximum number of CPUs to avoid failing thread creation.
            configuredCpuCount = CPU_SETSIZE;
        }

        cpu_set_t* pCpuSet = CPU_ALLOC(configuredCpuCount);
        if (pCpuSet == nullptr)
        {

Comment thread src/native/minipal/cpucount.c
@BrzVlad

BrzVlad commented May 12, 2026

Copy link
Copy Markdown
Member Author

@janvorli @jkotas I have a linux vm that has cpu hotplug enabled by default. On my machine, the contents of /sys/devices/system/cpu/possible is 0-127, even though the vm has only 4 cores. Following #126763, the runtime crashes immediately because sched_getaffinity fails. Now that the build is using preview 5 containing this change, the sdk is unusable on this VM. Fails with:

/home/vbrezae/Xamarin/repos/runtime2/.dotnet/sdk/11.0.100-preview.5.26227.104/NuGet.RestoreEx.targets(19,5): error : GC: This process is affinitize to 0 CPUs, check your GC heap affinity related configs [/home/vbrezae/Xamarin/repos/runtime2/Build.proj]
/home/vbrezae/Xamarin/repos/runtime2/.dotnet/sdk/11.0.100-preview.5.26227.104/NuGet.RestoreEx.targets(19,5): error : GC heap initialization failed with error 0x8013200A [/home/vbrezae/Xamarin/repos/runtime2/Build.proj]
/home/vbrezae/Xamarin/repos/runtime2/.dotnet/sdk/11.0.100-preview.5.26227.104/NuGet.RestoreEx.targets(19,5): error : Failed to create CoreCLR, HRESULT: 0x8013200A [/home/vbrezae/Xamarin/repos/runtime2/Build.proj]

There doesn't seem to be a way to obtain this value via the OS api. From my understanding, there is this ordering: possible cpus >= configured cpus >= online cpus. When using the sched_getaffinity API, we need to ensure we have space reserved for the number of possible cpus on the system, not just the currently present ones. To avoid code duplication I've added a small helper in minipal, only for unix since I didn't want to touch more code than necessary (for the windows path).

@jakobbotsch

Copy link
Copy Markdown
Member

What about code like

Debug.Assert(s_perCoreCache.Length == Environment.ProcessorCount, $"{s_perCoreCache.Length} != {Environment.ProcessorCount}");
int i = (int)((uint)Thread.GetCurrentProcessorId() % (uint)Environment.ProcessorCount);

? Do we expect to be able to cope with these values changing?

@BrzVlad

BrzVlad commented May 12, 2026

Copy link
Copy Markdown
Member Author

The doc at https://learn.microsoft.com/en-us/dotnet/api/system.environment.processorcount?view=netframework-4.8.1&viewFallbackFrom=net-10.0 suggests that this API makes no effort to deal with changes in configuration/affinity.

On unix, during initialisation, the runtime obtains the total number of CPUs via `sysconf(_SC_NPROCESSORS_CONF)`. This should return the current number of cpus that are currently present on the system. It turns out linux has cpu hotplug support, so this number can increase. When hotplug is enabled, the kernel reserves storage for the max possible number of CPUs. This max number is exported in `/sys/devices/system/cpu/possible`. The problem is that, when allocating the `cpu_set_t*` for use with `sched_getaffinity`, this api failed because the OS expected for the cpu set to have reserved space for the maximum amount of cpu's, not just for the ones that are currently present.
@janvorli

Copy link
Copy Markdown
Member

@BrzVlad that means the doc for sysconf is incorrect. It says that _SC_NPROCESSORS_ONLN would get the number of processors that are online and _SC_NPROCESSORS_CONF the number of processors configured in the system. The doc for get_nprocs, get_nprocs_conf that gets the same values say:

The function get_nprocs() returns the number of processors
currently available in the system. This may be less than the
number returned by get_nprocs_conf() because processors may be
offline (e.g., on hotpluggable systems).

This seems to indicate that the _SC_NPROCESSORS_CONF should include even CPUs that can be hot plugged, but are currently not.

@BrzVlad

BrzVlad commented May 12, 2026

Copy link
Copy Markdown
Member Author

Some more context. Fixed in glibc 2.36 -> bminor/glibc@97a912f

@janvorli

Copy link
Copy Markdown
Member

Ok, since we want to support even older glibc versions, we need to make this fix.

Copilot AI review requested due to automatic review settings May 12, 2026 14:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread src/native/minipal/CMakeLists.txt
Comment thread src/native/minipal/cpucount.c
Comment thread src/native/minipal/cpucount.c
Ex: 0-1,3-4,6,8-10. It is not obvious in which scenario the kernel would report this, better safe than sorry.
Copilot AI review requested due to automatic review settings May 12, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread src/native/minipal/CMakeLists.txt

@janvorli janvorli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

@janvorli
janvorli merged commit 7a71ac4 into dotnet:main May 12, 2026
181 checks passed
janvorli added a commit to janvorli/runtime that referenced this pull request May 21, 2026
…128069)

On unix, during initialization, the runtime obtains the total number of
CPUs via `sysconf(_SC_NPROCESSORS_CONF)`. This should return the current
number of cpus that are currently present on the system. It turns out
linux has cpu hotplug support, so this number can increase. When hotplug
is enabled, the kernel reserves storage for the max possible number of
CPUs. This max number is exported in `/sys/devices/system/cpu/possible`.
The problem is that, when allocating the `cpu_set_t*` for use with
`sched_getaffinity`, this api failed because the OS expected for the cpu
set to have reserved space for the maximum amount of cpu's, not just for
the ones that are currently present.
janvorli added a commit to janvorli/runtime that referenced this pull request May 21, 2026
…128069)

On unix, during initialization, the runtime obtains the total number of
CPUs via `sysconf(_SC_NPROCESSORS_CONF)`. This should return the current
number of cpus that are currently present on the system. It turns out
linux has cpu hotplug support, so this number can increase. When hotplug
is enabled, the kernel reserves storage for the max possible number of
CPUs. This max number is exported in `/sys/devices/system/cpu/possible`.
The problem is that, when allocating the `cpu_set_t*` for use with
`sched_getaffinity`, this api failed because the OS expected for the cpu
set to have reserved space for the maximum amount of cpu's, not just for
the ones that are currently present.
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants