macOS syscollector silently reports only kern.maxproc/4 processes because proc_listallpids is passed a process count instead of a buffer size in bytes
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:
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:
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, id006, macOS 26.5.1 (build 25F80), arm64, Apple M1 (Virtual), 2 vCPU, 4 GiB RAM, Parallels VM on MacStadium hostmacstadium_arm_4. - Agent package
wazuh-agent-5.0.0-latest.arm64.pkgfromnightly-backup/2026-09-09, receiptcom.wazuh.pkg.wazuh-agentversion 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.0branch ofwazuh/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
- 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:
sysctl -n kern.maxproc # 2000 here, so the ceiling is 500
ps -A | tail -n +2 | wc -l # 549 here- Force a fresh inventory evaluation and wait for it to finish:
sudo /Library/Ossec/bin/wazuh-control restart
grep "syscollector" /Library/Ossec/logs/ossec.log | tail -5
# ... INFO: Starting evaluation.
# ... INFO: Evaluation finished.- Count what the agent actually stored locally:
sudo sqlite3 /Library/Ossec/queue/syscollector/db/local.db \
"select count(*) from dbsync_processes;"- Compare against the indexer, aggregating the process state index by agent:
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}}}}'- Cross-check the other syscollector tables on the same agent against the host, to confirm the fault is specific to processes:
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 -wExpected
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 77Every 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 mismatchNothing 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.maxprocreturned nothing open.
Source: wazuh/wazuh