From 65f6fa908dcc7dfe79ce1a93559d51f4f054d039 Mon Sep 17 00:00:00 2001 From: Cropi Date: Mon, 10 Aug 2026 15:35:35 +0200 Subject: [PATCH] auditd: fix V0 dispatch truncating last 16 bytes of events When log_format=RAW and audisp-syslog is active, the last 16 characters of every kernel audit event were missing from syslog output. For example a USER event ending in "terminal=? res=success'" would appear as "termina" with the rest cut off. The problem was found by comparing the audit.log output (which was complete) with the syslog output (which was truncated). Tracing with strace showed that auditd's write to the plugin pipe was 16 bytes shorter than the write to the log file. Commit ba8ba4f2 subtracted NLMSG_HDRLEN (16) from nlmsg_len before copying the payload to the dispatcher, assuming nlmsg_len follows the standard netlink convention where it includes the 16-byte header. The kernel audit subsystem does not follow that convention. In audit_log_end() (kernel/audit.c) the kernel sets: nlh->nlmsg_len = skb->len - NLMSG_HDRLEN; This means nlmsg_len is already the payload size, not the total message size. Subtracting NLMSG_HDRLEN again removes 16 bytes of real event data. The kernel documents this as a non-standard choice. The multicast path (kauditd_send_multicast_skb) makes a copy and restores the standard nlmsg_len = skb->len for non-auditd listeners. A netlink probe confirmed this on kernel 6.12: for every event, recvfrom returns nlmsg_len + 16 bytes, meaning nlmsg_len equals the payload size and the extra 16 bytes are the header. Fix: use nlmsg_len directly as the payload size for both kernel and synthetic events, and keep a bounds check against the buffer size. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Cropi --- src/auditd-dispatch.c | 22 +++------- src/test/auditd_dispatch_test.c | 72 ++++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/auditd-dispatch.c b/src/auditd-dispatch.c index 40bed7c8e..db7a1f5ed 100644 --- a/src/auditd-dispatch.c +++ b/src/auditd-dispatch.c @@ -75,24 +75,14 @@ int dispatch_event(const struct audit_reply *rep, int protocol_ver) e->hdr.type = rep->type; if (protocol_ver == AUDISP_PROTOCOL_VER) { - if (rep->nlh == &rep->msg.nlh) { - if (rep->msg.nlh.nlmsg_len < NLMSG_HDRLEN || - rep->msg.nlh.nlmsg_len > - NLMSG_LENGTH(sizeof(e->data))) { - free(e); - return -1; - } - - /* audit_get_reply marks embedded netlink replies this - * way. Netlink length includes its header, while - * event_t contains only payload. Local V0/legacy - * events retain a payload length here. */ - e->hdr.size = rep->msg.nlh.nlmsg_len - NLMSG_HDRLEN; - } else if (rep->msg.nlh.nlmsg_len > sizeof(e->data)) { + /* The kernel audit subsystem sets nlmsg_len to the payload + * size only (skb->len - NLMSG_HDRLEN), not the total + * message length. */ + if (rep->msg.nlh.nlmsg_len > sizeof(e->data)) { free(e); return -1; - } else - e->hdr.size = rep->msg.nlh.nlmsg_len; + } + e->hdr.size = rep->msg.nlh.nlmsg_len; memcpy(e->data, (void*)rep->msg.data, e->hdr.size); } else if (protocol_ver == AUDISP_PROTOCOL_VER2) { e->hdr.size = rep->len; diff --git a/src/test/auditd_dispatch_test.c b/src/test/auditd_dispatch_test.c index dfdc169fe..265560a78 100644 --- a/src/test/auditd_dispatch_test.c +++ b/src/test/auditd_dispatch_test.c @@ -84,9 +84,10 @@ static void free_queued_event(void) } /* - * test_netlink_payload_length - verify netlink headers are not copied as data + * test_netlink_payload_length - kernel nlmsg_len is the payload size directly * - * Returns: None. + * The kernel audit subsystem sets nlmsg_len = skb->len - NLMSG_HDRLEN, + * i.e. the payload size only. Verify dispatch uses it as-is. */ static void test_netlink_payload_length(void) { @@ -96,7 +97,7 @@ static void test_netlink_payload_length(void) memset(rep.msg.data, 'a', sizeof(rep.msg.data)); rep.type = AUDIT_SYSCALL; rep.nlh = &rep.msg.nlh; - rep.msg.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(rep.msg.data)); + rep.msg.nlh.nlmsg_len = sizeof(rep.msg.data); assert(dispatch_event(&rep, AUDISP_PROTOCOL_VER) == 0); assert(queued_event != NULL); @@ -107,7 +108,7 @@ static void test_netlink_payload_length(void) } /* - * test_invalid_netlink_length - reject malformed embedded netlink lengths + * test_invalid_netlink_length - reject oversized payloads * * Returns: None. */ @@ -117,11 +118,7 @@ static void test_invalid_netlink_length(void) memset(&rep, 0, sizeof(rep)); rep.nlh = &rep.msg.nlh; - rep.msg.nlh.nlmsg_len = NLMSG_HDRLEN - 1; - assert(dispatch_event(&rep, AUDISP_PROTOCOL_VER) == -1); - assert(queued_event == NULL); - - rep.msg.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(rep.msg.data)) + 1; + rep.msg.nlh.nlmsg_len = sizeof(rep.msg.data) + 1; assert(dispatch_event(&rep, AUDISP_PROTOCOL_VER) == -1); assert(queued_event == NULL); } @@ -146,10 +143,67 @@ static void test_synthetic_payload_length(void) free_queued_event(); } +/* + * test_realistic_user_event - exercise a USER record like auditctl -m produces + * + * The kernel sets nlmsg_len to the payload size (not including the netlink + * header). The dispatcher must copy exactly that many bytes from msg.data. + */ +static void test_realistic_user_event(void) +{ + struct audit_reply rep; + const char *payload = + "audit(1721000000.123:42): pid=1234 uid=0 auid=0 ses=1 " + "subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 " + "msg='text=dispatch-test " + "exe=\"/usr/sbin/auditctl\" " + "hostname=? addr=? terminal=pts/0 res=success'"; + unsigned int plen = strlen(payload); + + memset(&rep, 0, sizeof(rep)); + rep.type = AUDIT_USER; + rep.nlh = &rep.msg.nlh; + rep.msg.nlh.nlmsg_len = plen; + memcpy(rep.msg.data, payload, plen); + + assert(dispatch_event(&rep, AUDISP_PROTOCOL_VER) == 0); + assert(queued_event != NULL); + assert(queued_event->hdr.size == plen); + assert(memcmp(queued_event->data, payload, plen) == 0); + free_queued_event(); +} + +/* + * test_v2_protocol_uses_rep_len - VER2 events use rep->len and rep->message + * + * Enriched / pre-formatted events travel as AUDISP_PROTOCOL_VER2 and must + * use the explicit rep->len field rather than nlmsg_len. + */ +static void test_v2_protocol_uses_rep_len(void) +{ + struct audit_reply rep; + const char *formatted = "type=USER msg=audit(1721000000.789:44): " + "op=user-msg terminal=pts/0 res=success"; + unsigned int flen = strlen(formatted); + + memset(&rep, 0, sizeof(rep)); + rep.type = AUDIT_USER; + rep.len = flen; + rep.message = (char *)formatted; + + assert(dispatch_event(&rep, AUDISP_PROTOCOL_VER2) == 0); + assert(queued_event != NULL); + assert(queued_event->hdr.size == flen); + assert(memcmp(queued_event->data, formatted, flen) == 0); + free_queued_event(); +} + int main(void) { test_netlink_payload_length(); test_invalid_netlink_length(); test_synthetic_payload_length(); + test_realistic_user_event(); + test_v2_protocol_uses_rep_len(); return 0; }