Skip to content
macOSDeep Dive Published Updated 10 min readViews unavailable

macOS auditpipe: Legacy BSM Streams, Queue Drops, and Migration

Understand auditpipe’s per-reader BSM queues and loss counters, then assess macOS deprecation and the migration boundary to Endpoint Security.

/dev/auditpipe is easy to mistake for a current, lossless macOS security-event API. It is neither. It exposes a live stream of selected Basic Security Module (BSM) audit records, with independent queues for each open device instance. Those queues can drop records, and Apple has deprecated the audit-event subsystem. This matters when you are maintaining an older collector, assessing a forensic artifact, or planning a migration; it is not a recommendation to start a new endpoint product on auditpipe.

Apple’s WWDC 2020 Endpoint Security session explicitly includes audit-trail files and the Auditpipe pseudo-device in the deprecated audit subsystem, and directs products that rely on audit events to Endpoint Security. The current macOS 27.0.1 man 8 audit page checked for this article says the subsystem has been deprecated since macOS 11.0, disabled since macOS 14.0, and “WILL BE REMOVED in a future version of macOS.” The notice is version-specific: run man 8 audit on the target Mac rather than assuming that an older guide’s activation steps describe current behavior. Do not re-enable a deprecated system service just to make a legacy collector appear healthy.

Read the macOS state before interpreting the device

The audit(4) subsystem produces BSM records according to its configured policy. When enabled, auditd manages the audit trail files; auditpipe(4) is a live observation path that the manual describes as a tee of the selected audit-event stream. It does not replace the trail or the system-wide policy, and it does not turn every system action into an event. This is not a general OpenBSM configuration walkthrough: do not transfer a FreeBSD audit-control recipe to macOS or infer that a shared BSM format means the host subsystems have the same lifecycle.

Selection is part of the contract. A newly opened pipe defaults to the system trail’s preselection settings. A client can instead configure pipe-local selection masks, including per-audit-user-ID masks. That changes what the particular consumer receives; it does not retroactively create records the kernel did not generate or recover events excluded before they reached that pipe. A consumer that changes its selection mode can still have records selected under the previous mode in its queue. The documented flush ioctl discards that pipe’s queued records; it is not a recovery operation.

The records are BSM token streams, not Unified Logging messages and not Endpoint Security es_message_t objects. The installed praudit(1) manual describes the decoder; BSM token definitions include subject and path tokens, so output may expose sensitive identities and file paths. Treat them as evidence with the record’s time and identity context, not as a live query of the object’s current state. Unified Logging is Apple’s structured diagnostic logging system, BSM is the legacy audit-record format, and Endpoint Security delivers typed event messages to an entitled client.

A pipe is a per-consumer queue, not a shared durable log

The device is clonable. Multiple consumers can open separate instances, and each instance has independent access to records and its own queue. That isolation is useful, but it means one process cannot inspect another consumer’s queue or loss counter just by opening /dev/auditpipe a second time. Query statistics through the same file descriptor whose stream you are monitoring.

The queue is bounded. If it fills, records are dropped. There is a source-level discrepancy around short reads: the pinned auditpipe(4) manual says a record is discarded when the supplied buffer is too small, but audit_pipe_read() in the matching pinned XNU source copies only the available portion and tracks an offset into the queued record. Do not generalize either behavior to every macOS build; use the runtime maximum-record-size ioctl to size the read buffer and test the exact target release if partial reads matter. In the pinned implementation, the insert counter increments when a record is enqueued, the drop counter increments when a record cannot be enqueued, and the read counter increments once per read(2) call that enters the read loop, not once per record returned. The ioctl header encodes all three statistics ioctls (AUDITPIPE_GET_INSERTS, AUDITPIPE_GET_READS, and AUDITPIPE_GET_DROPS) with a u_int64_t type, but the matching dispatcher writes each through u_int *. The example uses the SDK-declared buffer width; on that pinned implementation only the low 32 bits are populated. Treat the values as diagnostic counters, not complete 64-bit lifetime totals, and verify the exact target OS implementation. None of these counters proves that the system audit policy selected every event your product expected.

For a legacy reader, query the maximum record size and allocate at least that much before reading. Keep the read path bounded: do not synchronously upload, perform a slow name lookup, or hold the descriptor idle while expensive analysis runs. If a deployment supports changing a pipe’s queue limit, a larger limit buys burst headroom, not lossless delivery; it also consumes more memory. Track drop-counter deltas on the same descriptor and alert when a nonzero delta invalidates a completeness claim. A zero counter only describes that pipe’s observed lifetime, not the whole machine’s history.

Inspect without changing the host’s audit configuration

Start by checking what the installed system says, not by copying a recipe that changes a protected service:

sw_vers -productVersion
man 8 audit
man 4 auditpipe
man 1 praudit

On a legacy Mac where auditing is active and the caller is permitted to read the device, the auditpipe(4) manual documents praudit /dev/auditpipe as a way to inspect the live stream. That reader can block while waiting for records and display sensitive BSM tokens. A permission error is an access boundary, not a reason to loosen /dev/auditpipe permissions. On modern macOS, an existing device path or an installed praudit command does not prove that the deprecated audit subsystem is active. This article intentionally does not repeat a FreeBSD audit configuration recipe or recommend re-enabling the macOS service.

For code that needs to observe its own queue health, the following read-only probe queries counters on one open instance. It neither reads event payloads nor changes global audit policy. A long-running reader should retain this descriptor and query its counters periodically; reopening the device creates a different queue and a fresh set of per-pipe statistics. The labels distinguish inserted records, read syscalls, and dropped records; the read counter is not a count of records consumed. On the pinned implementation, all three values expose only the low 32 bits despite the wider header type, so use them as short-interval diagnostics and test the target release before building long-lived accounting on them. This sample returns status 2 when the reported drop value is nonzero; it does not indicate a system-wide audit failure.

#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <security/audit/audit_ioctl.h>

int
main(void)
{
    int fd = open("/dev/auditpipe", O_RDONLY);
    if (fd == -1) {
        perror("open /dev/auditpipe");
        return 1;
    }

    u_int queued = 0, queue_limit = 0, max_record_bytes = 0;
    u_int64_t inserts = 0, read_calls = 0, drops = 0;
    if (ioctl(fd, AUDITPIPE_GET_QLEN, &queued) == -1 ||
        ioctl(fd, AUDITPIPE_GET_QLIMIT, &queue_limit) == -1 ||
        ioctl(fd, AUDITPIPE_GET_MAXAUDITDATA, &max_record_bytes) == -1 ||
        ioctl(fd, AUDITPIPE_GET_INSERTS, &inserts) == -1 ||
        ioctl(fd, AUDITPIPE_GET_READS, &read_calls) == -1 ||
        ioctl(fd, AUDITPIPE_GET_DROPS, &drops) == -1) {
        perror("AUDITPIPE_GET_*");
        close(fd);
        return 1;
    }

    printf("queued=%u limit=%u max_record_bytes=%u inserted_records_reported=%" PRIu64
           " read_calls_reported=%" PRIu64 " dropped_records_reported=%" PRIu64 "\n",
           queued, queue_limit, max_record_bytes, inserts, read_calls, drops);
    close(fd);
    return drops ? 2 : 0;
}

The sample reports one consumer’s point-in-time state; it is not an audit-log validator. Check the manual and SDK header shipped with the deployment target before compiling, and handle open or ioctl failures as expected outcomes. The probe was syntax-checked against the local macOS 27 SDK. It was not run against the live device: on the test Mac, /dev/auditpipe is owned by root with mode 0600, and the audit subsystem is deprecated and disabled by default. No permission or service changes were attempted.

Implementation details in the cited Apple XNU source are pinned to xnu-12377.1.9 (commit f6217f891ac0bb64f3d375211650a4c1ff8ca1ea), not the exact kernel running on the verification Mac (macOS 27.0.1 (Darwin 27.0.0, XNU 13432.1.9)). Use the pinned source to understand the interface and loss model, not as proof of every current kernel default. The examples query limits at runtime instead of embedding queue-size constants; verify local manuals and headers for each supported release.

Diagnose the right kind of missing event

When a collector reports gaps, separate at least four questions. First, was the audit subsystem enabled on that OS release? Second, did the system-wide or pipe-local preselection policy include the event class? Third, did this consumer’s queue overflow, and what short-read behavior does the exact target release implement? Fourth, did the consumer parse and persist the BSM bytes correctly? AUDITPIPE_GET_DROPS can help with queue drops only, and only for the descriptor whose queue it measures; it cannot by itself resolve the manual/source discrepancy about short reads.

Do not use AUDITPIPE_FLUSH to hide a backlog and then describe the stream as complete. Do not infer that a second observer saw the same queue state. If you need a durable historical record on a legacy, enabled system, inspect the configured audit trail and its retention/rotation policy instead; a live pipe is explicitly not the reliable long-term store described by the manual. Preserve copies using your organization’s approved evidence-handling procedure, because audit output itself may contain sensitive identifiers and paths.

Migrate the requirement, not the ioctl list

Apple’s replacement guidance is Endpoint Security, not a mechanically translated auditpipe consumer. Start with the behavior the old product actually needs: which operation, whether it needs a notification after the operation or an authorization decision before it, which process identity fields it uses, and what loss or deadline behavior it can tolerate. Map those requirements to supported Endpoint Security event types and the APIs available for the minimum macOS versions you deploy. Apple describes Endpoint Security as a C event-monitoring API; using it requires the restricted entitlement, and a system-extension deployment has its own installation, approval, and privacy requirements.

Do not label every audit-related type obsolete. Apple’s WWDC guidance says audit tokens and audit sessions remain core system concepts even though the audit-event subsystem is deprecated. Endpoint Security process data still exposes an audit token for BSM-related identity helpers. The migration is therefore a change in event delivery and policy architecture, not a ban on the identity token itself. Keep the legacy reader isolated, version-gate any compatibility code, and test event coverage, client permissions, sequence/loss handling, and failure behavior on each supported OS before retiring the old path.

Related:

Sources:

Comments