fix(linux): respect cgroup cpuset when pinning fuzzing threads#539
fix(linux): respect cgroup cpuset when pinning fuzzing threads#539claudijd wants to merge 1 commit into
Conversation
|
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. |
f32b3a2 to
cad31ad
Compare
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.
cad31ad to
52e6e85
Compare
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):
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:
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.