Skip to content

Filter DEAD_PROCESS utmpx entries out of macOS logged-in users provider - #39313

Merged
cborla merged 2 commits into
4.14.9from
fix/39170-macos-logged-in-users-filter-dead-process
Sep 15, 2026
Merged

cborla merged 2 commits into
4.14.9from
fix/39170-macos-logged-in-users-filter-dead-process

Conversation

@rjcausarano

@rjcausarano rjcausarano commented Sep 15, 2026

Copy link
Copy Markdown
Member

Description

On macOS, LoggedInUsersProvider::collect() walks the utmpx database and returns every record except the one with ut_pid == 1, with no filter on ut_type. utmpx keeps DEAD_PROCESS entries for sessions that have already ended, so SysInfo::getUsers() (which matches purely by username, with no type check) can populate login_tty, process_pid and user_last_login from a session that is no longer running.

Closes #39170

Proposed Changes

  • src/data_provider/src/extended_sources/users/src/logged_in_users_darwin.cpp: LoggedInUsersProvider::collect() now skips any utmpx entry whose ut_type is not USER_PROCESS, in addition to the existing ut_pid == 1 skip. Only live sessions are emitted.
  • No changes to SysInfo::getUsers() (sysInfoMac.cpp) or the loginTypes label map — this is the darwin provider's only consumer, so narrowing what collect() returns is sufficient and keeps the diff minimal.

Results and Evidence

Reproduced the pre-fix defect live on a macOS 15.7.2 agent running Wazuh v4.14.7 (same bug, present since at least that branch). Full repro steps and query output: see this comment.

Summary: closed a short SSH pty session (ttys002), leaving a DEAD_PROCESS utmpx record newer than the two genuinely still-open sessions. After a syscollector rescan, the local cache showed:

sqlite3 /Library/Ossec/queue/syscollector/db/local.db \
  "SELECT user_name, login_status, login_tty, login_type, process_pid, user_last_login FROM dbsync_users WHERE login_type = 'dead';"

vagrant|1|ttys002|dead|13701|1789495267

login_status = 1 (reported logged in) via ttys002/pid 13701, both already dead at query time (ps -p 13701 → no such process; who showed no ttys002). This is getUsers() picking the newest-but-dead utmpx record over the older-but-live ones — exactly the failure mode #39170 describes.

Post-fix verification: repeated the identical reproduction against the same host upgraded to v4.14.9 with this fix applied. Full details: see this follow-up comment.

Summary: same steps (closed a short SSH pty session on ttys002, newer than the live ttys001 session), then rescanned:

sqlite3 ... "SELECT count(*) FROM dbsync_users WHERE login_type = 'dead';"
0

sqlite3 ... "SELECT user_name, login_status, login_tty, login_type, process_pid, user_last_login FROM dbsync_users WHERE user_name='vagrant';"
vagrant|1|ttys001|user|18253|1789499652

No dead-typed rows anywhere in the table, and vagrant's record correctly reflects the genuinely live ttys001 session (pid 18253, verified live via ps -p 18253) instead of the dead ttys002 one. Bug not reproducible post-fix.

The fix is additionally covered by two new unit tests (see "Tests Introduced" below), which mock the utmpx source directly with DEAD_PROCESS and USER_PROCESS entries and assert on collect()'s output.

Manual tests with their corresponding evidence

  • Compilation without warnings on every supported platform
    • Linux — N/A: logged_in_users_darwin.cpp is only added to the build under APPLE (src/data_provider/src/extended_sources/users/CMakeLists.txt), so it isn't compiled on Linux.
    • Windows — N/A: same reason, macOS-only source file.
    • MAC OS X — not yet run; no macOS build environment available in this session. Needed before merge.
  • Log syntax and correct language review
  • Memory tests for Linux

    • Coverity — N/A, file not compiled on Linux
    • Valgrind (memcheck and descriptor leaks check) — N/A, file not compiled on Linux
    • AddressSanitizer — N/A, file not compiled on Linux
  • Memory tests for Windows

    • Coverity — N/A, file not compiled on Windows
    • UMDH — N/A, file not compiled on Windows
  • Memory tests for macOS

    • Leaks — not yet run, needs a macOS environment
    • AddressSanitizer — not yet run, needs a macOS environment
  • Decoder/Rule tests (Wazuh v4.x) — N/A, no decoder/rule changes

    • Added unit testing files ".ini"
    • runtests.py executed without errors
  • Engine (Wazuh v5.x and above) — N/A, not an engine change

    • Test run in parallel
    • ASAN for test (utest/ctest)
    • TSAN for test and wazuh-engine.
  • Wazuh server API/Framework — N/A, not an API/framework change

    • Run API Integration Tests

Artifacts Affected

  • macOS agent binary (data_provider library consumed by syscollector's getUsers() inventory).

Configuration Changes

N/A — no configuration parameters added, removed, or changed.

Tests Introduced

Added to src/data_provider/src/extended_sources/users/tests/test_logged_in_users_darwin.cpp:

  • CollectFiltersOutDeadProcessEntries: a lone DEAD_PROCESS utmpx entry now yields an empty result from collect().
  • CollectKeepsLiveSessionAmongDeadOnes: given a mix of a DEAD_PROCESS and a USER_PROCESS entry, collect() returns only the live USER_PROCESS row.

Both mock IUtmpxWrapper the same way as the existing CollectReturnsExpectedJson test.

Review Checklist

  • Code changes reviewed
  • Relevant evidence provided
  • Tests cover the new functionality
  • Configuration changes documented
  • Developer documentation reflects the changes
  • Meets requirements and/or definition of done
  • No unresolved dependencies with other issues
  • ...

@rjcausarano rjcausarano changed the title Filters logged in users on MacOS fix: filter DEAD_PROCESS utmpx entries out of macOS logged-in users provider Sep 15, 2026
@rjcausarano rjcausarano changed the title fix: filter DEAD_PROCESS utmpx entries out of macOS logged-in users provider Filter DEAD_PROCESS utmpx entries out of macOS logged-in users provider Sep 15, 2026
@rjcausarano

Copy link
Copy Markdown
Member Author

Reproduced on a live 4.14.7 macOS agent

Confirmed the pre-fix defect is real (not just static analysis) on a macOS 15.7.2 agent running Wazuh v4.14.7.

Repro steps:

  1. Opened and immediately closed a short SSH pty session to the agent host (ssh -tt <host> "echo ...; sleep 1; exit"), which allocated ttys002 and left a DEAD_PROCESS utmpx record with a timestamp newer than the two genuinely still-open sessions (ttys000 since 06:00, ttys001 since 10:41).
  2. sudo /Library/Ossec/bin/wazuh-control restart to force a fresh syscollector scan.
  3. Queried the local syscollector cache directly:
sqlite3 /Library/Ossec/queue/syscollector/db/local.db \
  "SELECT user_name, login_status, login_tty, login_type, process_pid, user_last_login FROM dbsync_users WHERE user_name='vagrant';"

Result:

vagrant|1|ttys002|dead|13701|1789495267
  • login_status = 1 — inventory asserts vagrant is currently logged in.
  • login_type = dead on ttys002, pid 13701, last login 1789495267 (2026-09-15 11:01:07 PDT).

Cross-checked against ground truth at query time:

  • ps -p 13701 → no such process; the PID no longer exists.
  • who shows only console, ttys000, ttys001 as live — ttys002 is not among them.
  • last -n 3 confirms ttys002 started and ended within the same minute, well before the query.

So getUsers() picked the newest-but-dead utmpx record over the older-but-still-live sessions (unguarded newDate > lastLogin comparison), and reported it as the user's active login — the exact failure mode described in #39170, including the login_status=1 / login_type=dead self-contradiction that #39164 separately covers.

With this PR's fix (ut_type != USER_PROCESS filtered out in LoggedInUsersProvider::collect()), the dead ttys002 record would never reach getUsers() in the first place.

@rjcausarano

Copy link
Copy Markdown
Member Author

Post-fix verification on a live 4.14.9 agent (fixed code)

Repeated the exact same reproduction as the earlier pre-fix comment, this time against a macOS agent upgraded to v4.14.9 with this PR's fix applied.

Repro steps (identical to before):

  1. Confirmed the agent is on the fixed version:
    WAZUH_VERSION="v4.14.9"
    WAZUH_REVISION="alpha0"
    WAZUH_TYPE="agent"
    
  2. Opened and immediately closed a short SSH pty session (ssh -tt <host> "echo ...; sleep 1; exit"), which allocated ttys002 and left a DEAD_PROCESS utmpx record timestamped after the genuinely still-open session on ttys001:
    vagrant    ttys002  192.168.64.1           Tue Sep 15 12:24 - 12:24  (00:00)
    vagrant    ttys001  192.168.64.1           Tue Sep 15 12:14   still logged in
    
  3. sudo /Library/Ossec/bin/wazuh-control restart to force a fresh syscollector scan.
  4. Queried the local syscollector cache:
    sqlite3 /Library/Ossec/queue/syscollector/db/local.db \
      "SELECT user_name, login_status, login_tty, login_type, process_pid, user_last_login FROM dbsync_users WHERE user_name='vagrant';"
    
    vagrant|1|ttys001|user|18253|1789499652
    
    sqlite3 ... "SELECT count(*) FROM dbsync_users WHERE login_type = 'dead';"
    
    0
    

Result: bug no longer reproducible.

  • login_type = 'dead' returns zero rows across the whole table — the dead ttys002 record never reached the inventory.
  • vagrant's row correctly reflects the genuinely live session instead: login_tty=ttys001, login_type=user, process_pid=18253, user_last_login=1789499652 (2026-09-15 12:14:12 PDT, matching the ttys001 login time).
  • Cross-checked: ps -p 18253sshd-session: vagrant@ttys001 (a real, live process), consistent with who.

This matches the expected effect of the fix: LoggedInUsersProvider::collect() filters out DEAD_PROCESS entries at the source, so SysInfo::getUsers() never sees the dead ttys002 record and correctly falls back to the still-live ttys001 session instead of picking the newest-but-dead one.

…ilter

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@cborla cborla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

  • Code changes reviewed
  • Relevant evidence provided
  • Tests cover the new functionality
  • Configuration changes documented
  • Developer documentation reflects the changes
  • Meets requirements and/or definition of done
  • No unresolved dependencies with other issues

@cborla
cborla merged commit 9724fc6 into 4.14.9 Sep 15, 2026
40 checks passed
@cborla
cborla deleted the fix/39170-macos-logged-in-users-filter-dead-process branch September 15, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS logged-in users provider returns DEAD_PROCESS utmpx entries, so ended sessions are reported as a user's login session

2 participants