No description
  • C 40%
  • Shell 38.7%
  • Dockerfile 21.3%
Find a file
nyyu a0fac2e18c
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: update Debian base image from stable to bookworm for glibc build
2026-01-28 07:49:08 +01:00
.woodpecker.yml feat: consolidate Dockerfile for musl and glibc builds, remove Debian support, and enhance copy script 2026-01-27 22:14:40 +01:00
copy.sh feat: consolidate Dockerfile for musl and glibc builds, remove Debian support, and enhance copy script 2026-01-27 22:14:40 +01:00
Dockerfile fix: update Debian base image from stable to bookworm for glibc build 2026-01-28 07:49:08 +01:00
numcpu_override.c fix: update Debian base image from stable to bookworm for glibc build 2026-01-28 07:49:08 +01:00
README.md feat: consolidate Dockerfile for musl and glibc builds, remove Debian support, and enhance copy script 2026-01-27 22:14:40 +01:00
renovate.json chore(renovate): use common conf 2024-05-03 12:19:33 +02:00

numcpu_override

A lightweight shared library that intercepts sysconf() system calls to override the reported CPU count. This allows applications to see a different number of CPUs than physically available, useful for testing, resource limiting, and container environments.

Features

  • Overrides _SC_NPROCESSORS_CONF and _SC_NPROCESSORS_ONLN
  • Works with any application using sysconf() for CPU detection
  • Builds both musl and glibc variants in one Docker build
  • Optional debug logging
  • Input validation and error handling
  • Zero overhead when not overriding

How It Works

The library uses LD_PRELOAD to intercept sysconf() calls and returns the value specified in the NUMCPUS environment variable instead of the actual CPU count.

Why Two Libraries?

musl vs glibc are different C standard library implementations with incompatible ABIs (Application Binary Interfaces). A shared library compiled against one cannot be used with the other:

  • musl: Used by Alpine Linux and other minimal distributions
  • glibc: Used by Debian, Ubuntu, RHEL, Fedora, and most mainstream Linux distributions

The Docker build creates both variants so you can use the library on any Linux system. Use the matching variant for your target environment.

Building

Local Build

# Alpine/musl
gcc -O2 -shared -fPIC -Wall -Wextra -Werror numcpu_override.c -o numcpu_override-musl.so -ldl

# Debian/glibc
gcc -O2 -shared -fPIC -Wall -Wextra -Werror numcpu_override.c -o numcpu_override-glibc.so -ldl

Docker Build

The Dockerfile builds both musl and glibc variants in parallel stages:

# Build both libraries in one command
docker build -t numcpu-override .

# Option 1: Extract both libraries to organized subdirectories
mkdir -p _libs
docker run --rm -v $(pwd)/_libs:/_libs numcpu-override
# Result:
#   _libs/musl/numcpu_override.so       (for Alpine, musl-based distros)
#   _libs/glibc/numcpu_override.so      (for Debian, Ubuntu, most Linux distros)

# Option 2: Extract only the variant you need using TARGET_LIBC
# For Alpine/musl systems:
docker run --rm -e TARGET_LIBC=musl -v $(pwd)/_libs:/_libs numcpu-override
# Result: _libs/numcpu_override.so (musl variant)

# For Debian/Ubuntu/glibc systems:
docker run --rm -e TARGET_LIBC=glibc -v $(pwd)/_libs:/_libs numcpu-override
# Result: _libs/numcpu_override.so (glibc variant)

Choosing the Right Library

Your System Use This Library Common Distros
musl numcpu_override-musl.so Alpine Linux, Void Linux (musl)
glibc numcpu_override-glibc.so Debian, Ubuntu, RHEL, Fedora, CentOS, Arch

How to check your system:

ldd --version 2>&1 | head -n1
# musl: "musl libc"
# glibc: "ldd (GNU libc)" or "ldd (Ubuntu GLIBC..."

Usage

Basic Usage

# On glibc systems (Debian, Ubuntu, etc.)
NUMCPUS=4 LD_PRELOAD=./_libs/glibc/numcpu_override.so ./your_application

# On musl systems (Alpine)
NUMCPUS=4 LD_PRELOAD=./_libs/musl/numcpu_override.so ./your_application

# Or export for multiple commands
export NUMCPUS=4
export LD_PRELOAD=/path/to/numcpu_override.so
./your_application

Docker/Container Usage

# Alpine-based container (musl)
FROM alpine:3.23
COPY _libs/musl/numcpu_override.so /usr/local/lib/
ENV LD_PRELOAD=/usr/local/lib/numcpu_override.so
ENV NUMCPUS=2

# Debian-based container (glibc)
FROM debian:stable
COPY _libs/glibc/numcpu_override.so /usr/local/lib/
ENV LD_PRELOAD=/usr/local/lib/numcpu_override.so
ENV NUMCPUS=2

# Or at runtime
docker run -e NUMCPUS=4 -e LD_PRELOAD=/path/to/numcpu_override.so ...

Python Example

# Detect your libc first
LIBC=$(ldd --version 2>&1 | grep -q musl && echo "musl" || echo "glibc")

# Run with appropriate library
NUMCPUS=2 LD_PRELOAD=./_libs/${LIBC}/numcpu_override.so python3 -c "import os; print(os.cpu_count())"
# Output: 2

Debug Mode

Enable debug logging to see when the override is triggered:

export NUMCPUS_DEBUG=1
export NUMCPUS=4
export LD_PRELOAD=./numcpu_override.so
./your_application

Debug output goes to stderr:

numcpu_override: intercepting sysconf(84)
numcpu_override: NUMCPUS=4
numcpu_override: returning 4 CPUs

Environment Variables

Variable Description Default
NUMCPUS Number of CPUs to report (1-65536) Real CPU count
NUMCPUS_DEBUG Enable debug logging (set to "1") Disabled
TARGET_LIBC Extract specific library: musl or glibc (Docker only) Both

Behavior

  • If NUMCPUS is not set or empty → returns actual CPU count
  • If NUMCPUS is invalid (negative, non-numeric, >65536) → returns actual CPU count and logs error
  • If NUMCPUS is valid → returns the specified value
  • Debug messages only appear when NUMCPUS_DEBUG=1

Use Cases

Testing Multi-Core Applications

Test how your application behaves with different CPU counts without needing multiple machines.

Resource Limiting

Make applications believe they have fewer CPUs available, useful for:

  • Fair resource sharing in containers
  • Limiting parallel execution
  • Testing resource-constrained scenarios

Build Systems

Control parallelism in build tools that auto-detect CPU count:

NUMCPUS=2 LD_PRELOAD=./numcpu_override.so make -j$(nproc)

Container Environments

Override CPU count in containers where cgroup limits don't properly restrict CPU detection.

Compatibility

Supported:

  • Linux x86_64, ARM64 (with matching architecture build)
  • musl-libc systems: Alpine Linux, Void Linux (musl)
  • glibc systems: Debian, Ubuntu, RHEL, Fedora, CentOS, Arch Linux
  • Applications using sysconf(_SC_NPROCESSORS_CONF) or sysconf(_SC_NPROCESSORS_ONLN)
  • Python, Java, Node.js, Go (with cgo), C/C++ applications

Not Supported:

  • Static binaries (no dynamic linking means no LD_PRELOAD)
  • Applications reading /proc/cpuinfo directly
  • cgroup CPU quotas (kernel-level limits)
  • Cross-libc usage (musl library on glibc system or vice versa)

Technical Details

Intercepted Calls

  • _SC_NPROCESSORS_CONF - Number of configured processors
  • _SC_NPROCESSORS_ONLN - Number of processors currently online

Implementation

  • Uses dlsym(RTLD_NEXT, "sysconf") to chain to the real implementation
  • Thread-safe environment variable reading
  • Validates input range (0-65536)
  • Falls back gracefully on invalid input
  • Minimal performance overhead

Troubleshooting

Override not working?

  • Verify LD_PRELOAD points to the correct .so file
  • Use the correct libc variant (musl vs glibc) - this is the most common issue!
  • Check the application uses sysconf() for CPU detection
  • Enable NUMCPUS_DEBUG=1 to verify interception

Application crashes?

  • Wrong libc variant - make sure you use musl for Alpine, glibc for Debian/Ubuntu
  • Check library architecture matches (x86_64, arm64, etc.)
  • Verify dynamic linking: ldd /path/to/numcpu_override.so

How to identify your libc:

ldd --version 2>&1 | head -n1
# musl libc (x86_64) -> use musl variant
# ldd (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35 -> use glibc variant

Wrong CPU count?

  • Some applications cache CPU count at startup
  • Some use /proc/cpuinfo instead of sysconf()
  • Some are statically linked and ignore LD_PRELOAD

License

See LICENSE file for details.

Contributing

Issues and pull requests welcome!