#39126·wazuh

macOS syscollector silently reports only kern.maxproc/4 processes because proc_listallpids is passed a process count instead of a buffer size in bytes

Author: RebitsCreated Sep 10, 2026Updated Sep 18, 2026
Labelstype/bugreporter/qalevel/task

Description

On macOS the agent's process inventory is silently truncated. The host runs about 550 processes, but syscollector collects exactly 500, both in the agent's own local database and in the indexer. No error or warning is logged, and the evaluation reports success.

The cap is not a coincidence and not a configured limit. proc_listallpids takes its second argument as a buffer size in bytes, but src/data_provider/src/sysInfoMac.cpp passes maxProc, which is the process count read from kern.maxproc:

cpp
const auto spPids         { std::make_unique<pid_t[]>(maxProc) };
const auto processesCount { proc_listallpids(spPids.get(), maxProc) };

for (int index = 0; index < processesCount; ++index)
{
    ProcessTaskInfo taskInfo{};
    const auto pid { spPids.get()[index] };
    ...
}

The buffer is correctly allocated as maxProc * sizeof(pid_t) bytes, but only maxProc bytes are declared to the kernel. proc_listallpids therefore fills at most maxProc / sizeof(pid_t) entries and returns that count, so the effective ceiling is kern.maxproc / 4.

On the host under test the arithmetic matches the observation exactly:

kern.maxproc      : 2000
sizeof(pid_t)     : 4
maxproc/4         : 500      <-- exactly the number collected
actual processes  : 549-560  (stable across samples)

Between 9% and 11% of running processes are therefore invisible on this host, and the shortfall grows on any macOS host whose real process count exceeds kern.maxproc / 4. Which processes are lost is not predictable: it is whatever proc_listallpids happens to place in the truncated fill, so the inventory is missing an arbitrary subset rather than, say, the newest entries. For a security product this is a detection blind spot, and it also skews anything downstream that correlates against process inventory.

The same defect appears at a second call site in the same file, in the code that walks each process's file descriptors to build the socket and port inventory:

cpp
auto pids { std::make_unique<pid_t[]>(maxProcess) };
const auto processesCount { proc_listallpids(pids.get(), maxProcess) };

Sockets owned by processes beyond the truncation point cannot be seen by that path. The macOS agent reports 7 ports against 13 real TCP sockets on the host, which is consistent with this, but I did not isolate the port undercount to this root cause, so treat that as a likely secondary impact rather than a confirmed one.

Environment

  • Agent under test: demo-env-macos-agent, id 006, macOS 26.5.1 (build 25F80), arm64, Apple M1 (Virtual), 2 vCPU, 4 GiB RAM, Parallels VM on MacStadium host macstadium_arm_4.
  • Agent package wazuh-agent-5.0.0-latest.arm64.pkg from nightly-backup/2026-09-09, receipt com.wazuh.pkg.wazuh-agent version 5.0.0.
  • Agent VERSION.json: {"version": "5.0.0", "stage": "rc1", "commit": "2161e69"}.
  • QA demo environment wqa-prod-58-demo-environment: clustered managers, one indexer, one dashboard, six enrolled agents. Not a container deployment, so there are no image digests to record.
  • Source inspected on the 5.0.0 branch of wazuh/wazuh, src/data_provider/src/sysInfoMac.cpp.
  • Control agents: demo-env-linux-deb-amd64-agent (002), demo-env-linux-deb-arm64-agent (001), demo-env-linux-rpm-amd64-agent (003), demo-env-linux-rpm-arm64-agent (004), demo-env-windows-agent (005).

Steps to reproduce

  1. Enroll a 5.0.0 macOS agent on a host whose running process count exceeds kern.maxproc / 4. Confirm the ceiling and the real count:
bash
sysctl -n kern.maxproc                    # 2000 here, so the ceiling is 500
ps -A | tail -n +2 | wc -l                # 549 here
  1. Force a fresh inventory evaluation and wait for it to finish:
bash
sudo /Library/Ossec/bin/wazuh-control restart
grep "syscollector" /Library/Ossec/logs/ossec.log | tail -5
# ... INFO: Starting evaluation.
# ... INFO: Evaluation finished.
  1. Count what the agent actually stored locally:
bash
sudo sqlite3 /Library/Ossec/queue/syscollector/db/local.db \
     "select count(*) from dbsync_processes;"
  1. Compare against the indexer, aggregating the process state index by agent:
bash
curl -sk --cert admin.pem --key admin-key.pem \
  "https://<indexer>:9200/wazuh-states-inventory-processes/_search?size=0" \
  -H 'Content-Type: application/json' \
  -d '{"aggs":{"a":{"terms":{"field":"wazuh.agent.name","size":20}}}}'
  1. Cross-check the other syscollector tables on the same agent against the host, to confirm the fault is specific to processes:
bash
for t in dbsync_users dbsync_groups dbsync_network_iface; do
  printf "%s " "$t"; sudo sqlite3 /Library/Ossec/queue/syscollector/db/local.db "select count(*) from $t;"
done
dscl . -list /Users | wc -l ; dscl . -list /Groups | wc -l ; ifconfig -l | wc -w

Expected

Syscollector enumerates every running process, so the count in dbsync_processes and in wazuh-states-inventory-processes tracks the host's real process count. If enumeration ever is truncated, it is reported rather than silent.

Actual

The count is pinned at exactly kern.maxproc / 4 and does not move when a fresh full evaluation runs against a stable host:

host ps BEFORE :      551
db rows BEFORE : 500
host ps AFTER  :      551
db rows AFTER  : 500
--- last syscollector lines ---
2026/09/10 06:55:57 wazuh-modulesd:syscollector: INFO: Started (pid: 82711).
2026/09/10 06:55:57 wazuh-modulesd:syscollector: INFO: Starting evaluation.
2026/09/10 06:56:00 wazuh-modulesd:syscollector: INFO: Evaluation finished.

The indexer agrees with the agent, and every other platform sits below the truncation point and so matches its host:

  macos-agent                             500     <-- host has ~550
  linux-deb-arm64-agent                   143
  linux-rpm-arm64-agent                   134
  linux-deb-amd64-agent                   116
  linux-rpm-amd64-agent                   102
  windows-agent                            77

Every other syscollector table on the same macOS agent matches the host exactly, which rules out a general collection or sync fault and isolates the defect to process enumeration:

  dbsync_users        134   vs  dscl . -list /Users   134
  dbsync_groups       163   vs  dscl . -list /Groups  163
  dbsync_network_iface 11   vs  ifconfig -l            11
  dbsync_processes    500   vs  ps -A                 549   <-- only mismatch

Nothing configures a 500 limit. DEFAULT_SYSCOLLECTOR_PROCESSES_LIMIT in src/shared/include/module_limits.h is 0 (unlimited), the manager's processes_limit in src/remoted/src/config.c defaults to 30000, and neither the manager's wazuh-manager-internal-options.conf nor the agent's internal_options.conf / local_internal_options.conf sets any syscollector limit.

One confirmation I could not perform: kern.maxproc is read-only at runtime on macOS 26 (sysctl -w kern.maxproc=4000 returns Invalid argument), so I could not vary the ceiling and watch the collected count follow it. The evidence for the mechanism is therefore the exact arithmetic match, the source code at both call sites, deterministic reproduction across a forced rescan, and the cross-platform and cross-table controls above.

Not a duplicate of

qa-known-issues.py check scored two cache candidates, and the live search returned nothing:

  • wazuh/wazuh-qa-automation#8762 "Syscollector VD synchronization failed" (0.50) is the transient vulnerability-detection sync warning emitted once per agent at first enrollment, which recovers on the next cycle. It lives in the QA automation repo, concerns the VD sync handshake rather than process enumeration, and has no bearing on how many processes are collected.
  • wazuh/wazuh#4605 "The Syscollector module uses and reports localtime instead of UTC" (0.43) is a timestamp timezone defect in syscollector output. Same module, but a different field, a different platform scope, and no relation to truncated enumeration.
  • Live search for proc_listallpids syscollector kern.maxproc returned nothing open.