Skip to content

fix(linux): respect cgroup cpuset when pinning fuzzing threads#539

Open
claudijd wants to merge 1 commit into
google:masterfrom
claudijd:fix/pin-thread-respect-cgroup-affinity
Open

fix(linux): respect cgroup cpuset when pinning fuzzing threads#539
claudijd wants to merge 1 commit into
google:masterfrom
claudijd:fix/pin-thread-respect-cgroup-affinity

Conversation

@claudijd

@claudijd claudijd commented Jul 3, 2026

Copy link
Copy Markdown

Summary

util_PinThreadToCPUs() (libhfcommon/util.c) computes the total CPU count via sysconf(_SC_NPROCESSORS_ONLN) and pins each fuzzing thread to (threadNo * cpucnt) % num_cpus, assuming CPU IDs 0..num_cpus-1 are all free and directly usable.

sysconf(_SC_NPROCESSORS_ONLN) reports the raw online CPU count and does not respect cgroup cpuset restrictions. Under a container/cgroup that limits CPU via quota/shares but not cpuset (a common configuration for containerized CI and fuzzing infrastructure), every concurrent honggfuzz instance on the same host independently computes the same "available" CPU count and pins its threads to the same low-numbered physical CPUs. This turns --pin_thread_cpu into cross-instance contention instead of the per-thread isolation it's meant to provide, while the rest of the host's CPUs sit idle.

It gets worse on a host with more CPUs than the container's cgroup actually allows: the raw index computed this way can fall entirely outside the cgroup's allowed set, since sysconf() has no way to know about it.

Fix

Query sched_getaffinity() to get the actual set of CPUs this process can be scheduled on, enumerate the real CPU IDs from that mask, and index into that list instead of assuming a contiguous 0..num_cpus-1 range. This is scoped to Linux only (_HF_ARCH_LINUX) since that's where cgroups apply; other platforms (FreeBSD, NetBSD, Solaris) are untouched and keep their existing sysconf()-based behavior.

This change is a no-op wherever the process is unconstrained (the common case, where the affinity mask already lists CPUs 0..num_cpus-1 in order), and correct wherever the process is cpuset-restricted.

Quick reproduction for reviewers

No Docker or root needed -- taskset alone sets the same sched_getaffinity-visible mask this fix reads, so it reproduces the bug/fix on any Linux box with 4+ logical CPUs (adjust the CPU numbers below to fit your machine's actual core count if it has fewer):

make
cd examples/badcode
gcc -o targets/badcode1 targets/badcode1.c
taskset -c 1,3 ../../honggfuzz --pin_thread_cpu 1 --threads 2 -N 200000 -i inputfiles -- targets/badcode1 ___FILE___ &
HFPID=$!
sleep 1
for tid in /proc/$HFPID/task/*; do grep -H Cpus_allowed_list $tid/status; done
kill $HFPID

Expected output on this (fixed) branch: the two worker threads show Cpus_allowed_list: 1 and Cpus_allowed_list: 3 respectively -- one each, both from the restricted set.

I ran the identical steps against genuine upstream master (unpatched) to confirm what reviewers will actually see there. It's silent, not a warning: thread #0 came back Cpus_allowed_list: 0 -- landing completely outside the {1,3} restriction, since the affinity computation uses sysconf()'s view of all 12 host CPUs rather than the taskset-restricted set. Thread #1 happened to land on CPU 1, which is inside {1,3} purely by chance (the modulo arithmetic can coincidentally overlap depending on thread count and host size). No pthread_setaffinity_np warning appears in either case -- the call succeeds every time, it just succeeds at pinning to the wrong CPU, which is what makes this easy to miss without deliberately checking /proc//task//status like this.

Verification

Also built and tested this fix specifically (not re-verifying the unpatched case here, see above for that) on Linux via Docker (Ubuntu, gcc, matching the project's own Dockerfile build), using a real container cgroup (--cpuset-cpus=1,3) rather than plain taskset, to confirm the fix works the same way under actual kernel-enforced cgroup restriction, not just a soft taskset-set mask. Same target, same flags (--pin_thread_cpu 1 --threads 2):

Live inspection of /proc//task//status during a real run confirms:

thread #0 (tid 3843): Cpus_allowed_list: 1
thread #1 (tid 3844): Cpus_allowed_list: 3

Each of the two fuzzing threads landed on a distinct, valid CPU from within the container's actual restricted cpuset -- not a raw host-wide index. The main/watchdog threads (unpinned by design) correctly show Cpus_allowed_list: 1,3, i.e. the full container-allowed set.

No warnings or errors from sched_getaffinity()/pthread_setaffinity_np() during the run. Full honggfuzz build succeeds cleanly with the project's strict -Wall -Wextra -Werror flags.

Context

I found this while investigating a similar bug in an unrelated fuzzing harness (LibAFL-based) that also assumed CPU IDs 0..N-1 were free rather than querying actual availability, causing severe cross-container contention on shared fuzzing infrastructure. While auditing other fuzzer engines used in that same infrastructure for the same class of bug, I found this dormant but real vulnerability in honggfuzz's own --pin_thread_cpu implementation. It isn't triggered unless a caller explicitly passes --pin_thread_cpu (the default is 0, meaning no pinning), but the underlying CPU-detection logic is incorrect for any containerized environment regardless of who calls it.

@google-cla

google-cla Bot commented Jul 3, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@claudijd

claudijd commented Jul 3, 2026

Copy link
Copy Markdown
Author

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Just submitted the CLA.

@claudijd claudijd force-pushed the fix/pin-thread-respect-cgroup-affinity branch from f32b3a2 to cad31ad Compare July 3, 2026 15:16
util_PinThreadToCPUs() computed the total CPU count via
sysconf(_SC_NPROCESSORS_ONLN) and pinned each thread to
(threadNo * cpucnt) % num_cpus, assuming CPU IDs 0..num_cpus-1 are all
free and usable. sysconf(_SC_NPROCESSORS_ONLN) reports the raw online
CPU count and does not respect cgroup cpuset restrictions, so under a
container/cgroup that limits CPU via quota/shares but not cpuset (a
common configuration), every concurrent honggfuzz instance on the same
host independently computes the same "available" CPU count and pins
its threads to the same low-numbered physical CPUs -- turning
--pin_thread_cpu into cross-instance contention instead of the
per-thread isolation it's meant to provide, while the rest of the
host's CPUs sit idle. Worse than that, on a host with more CPUs than
the container's cgroup actually allows, the raw index computed this
way can fall entirely outside the cgroup's allowed set, since
sysconf() has no way to know about it.

Query sched_getaffinity() and enumerate the actual CPU IDs this
process can be scheduled on, then index into that real list instead of
raw 0..num_cpus-1. This is a no-op wherever the process is
unconstrained (the common case, where the affinity mask already lists
CPUs 0..num_cpus-1 in order), and correct wherever it's
cpuset-restricted. Scoped to Linux only (_HF_ARCH_LINUX); other
platforms are unaffected and keep their existing behavior.

Verified on Linux (Docker) with a container restricted to
--cpuset-cpus=1,3 and --pin_thread_cpu 1 --threads 2: before this
change the thread affinity computation would use the full host CPU
count and very likely target CPU IDs outside {1,3} entirely; after
this change, live /proc/<pid>/task/<tid>/status inspection during a
real fuzzing run confirms thread #0 pinned to CPU 1 and thread google#1
pinned to CPU 3 -- exactly the two CPUs the cgroup actually allows,
each thread getting a distinct one.
@claudijd claudijd force-pushed the fix/pin-thread-respect-cgroup-affinity branch from cad31ad to 52e6e85 Compare July 3, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant