rsyslog: fix imjournal ratelimiter data race

imjournal keeps one module-global ratelimiter but runs one reader thread
per journal: every configured input() plus the implicit listener that
activateCnf() unconditionally adds via addListner(NULL, ...). All of
them call ratelimitAddMsg() on that single object, yet the module
never calls ratelimitSetThreadSafe(), so bThreadSafe stays 0 and
every lock in runtime/ratelimit.c is skipped.

With $RepeatedMsgReduction disabled this only races on scalar counters
in withinRatelimit() and merely yields imprecise numbers. Enabling the
directive additionally activates doLastMessageRepeatedNTimes(), which
mutates the shared ratelimit->pMsg pointer: one thread can free that
message in msgDestruct() while another is still dereferencing it in the
duplicate comparison. Message refcounts are atomic, so the count itself
does not tear, but nothing protects the pointer, and the result is a
use-after-free.

This is not fixed upstream: rsyslog master still never calls
ratelimitSetThreadSafe() from imjournal and still gates all locking on
bThreadSafe, so an uprev does not help.

ratelimitSetNoTimeCache() already calls pthread_mutex_init() on the same
mutex, so no extra initialisation is needed. The duplicate init inside
ratelimitSetThreadSafe() is harmless because runInput() runs before any
reader thread is spawned.

Needed before $RepeatedMsgReduction can be enabled; see the
gbmc-internal change enabling it in meta-google-gbmc/.../client.conf.

Tested: built for an armv7l gBMC target and deployed the patched
imjournal.so to a test BMC. The plugin loads cleanly (no undefined
symbols, both in:imjournal threads present) and duplicate suppression
still works -- 6 identical messages are reported as "message repeated
11 times", the expected 2N-1 for two readers sharing one ratelimiter.

Crash reproduction, 6000-message rounds with $RepeatedMsgReduction on:

  unpatched, 24000 messages: 3 SIGSEGV, coredumps, rsyslog.service
      "Main process exited, code=killed, status=11/SEGV"
  unpatched, RMR off, 28000 messages: 0 crashes
  patched, 48000 messages over two runs: 0 crashes, 0 restarts,
      0 coredumps

BMC restored to its original imjournal.so and config after each run
(md5 match, service active).

Fusion-Link: fusion2 N/A
Google-Bug-Id: 561749907
Change-Id: Ibc687516f5fc3fa897ba362eefae034a67c3c97f
Signed-off-by: Eyal Ron <eyalron@google.com>
TAG=agy
CONV=31661fa9-028b-493b-8dec-1c801e2b0023
diff --git a/recipes-extended/rsyslog/rsyslog/0004-imjournal-make-the-shared-ratelimiter-thread-safe.patch b/recipes-extended/rsyslog/rsyslog/0004-imjournal-make-the-shared-ratelimiter-thread-safe.patch
new file mode 100644
index 0000000..3efbae8
--- /dev/null
+++ b/recipes-extended/rsyslog/rsyslog/0004-imjournal-make-the-shared-ratelimiter-thread-safe.patch
@@ -0,0 +1,79 @@
+From: Eyal Ron <eyalron@google.com>
+Date: Mon, 15 Sep 2026 00:00:00 +0000
+Subject: [PATCH 4/4] imjournal: make the shared ratelimiter thread-safe
+Upstream-Status: Pending
+
+imjournal keeps a single module-global ratelimiter but runs one reader
+thread per journal: every configured input() plus the implicit listener
+that activateCnf() unconditionally adds via addListner(NULL, ...). All of
+them call ratelimitAddMsg() on that one object, yet the module never
+calls ratelimitSetThreadSafe(), so bThreadSafe stays 0 and every lock in
+runtime/ratelimit.c is skipped.
+
+With $RepeatedMsgReduction disabled this only races on scalar counters in
+withinRatelimit() and merely yields imprecise numbers. Enabling the
+directive additionally activates doLastMessageRepeatedNTimes(), which
+mutates the shared ratelimit->pMsg pointer:
+
+    if(ratelimit->pMsg != NULL && ...getMSG(ratelimit->pMsg)... ) {
+        ratelimit->nsupp++;
+        msgDestruct(&ratelimit->pMsg);
+        ratelimit->pMsg = pMsg;
+    } else {
+        if(ratelimit->pMsg != NULL) {
+            if(ratelimit->nsupp > 0) *ppRepMsg = ratelimitGenRepMsg(ratelimit);
+            msgDestruct(&ratelimit->pMsg);
+        }
+        ratelimit->pMsg = MsgAddRef(pMsg);
+    }
+
+One thread can free that message while another is still dereferencing it.
+Message refcounts are atomic, so the count itself does not tear, but
+nothing protects the pointer. The result is a use-after-free.
+
+Reproduced on gBMC (rsyslog 8.2502.0, armv7l): with $RepeatedMsgReduction
+enabled, rsyslogd took SIGSEGV and dumped core in 3 of 4 stress runs,
+while control runs with the directive disabled survived twice the message
+volume without a single crash.
+
+ratelimitSetNoTimeCache() already calls pthread_mutex_init() on the same
+mutex, so no extra initialisation is required. The duplicate init in
+ratelimitSetThreadSafe() is harmless here because runInput() runs before
+any reader thread is spawned.
+
+Signed-off-by: Eyal Ron <eyalron@google.com>
+---
+ plugins/imjournal/imjournal.c | 18 ++++++++++++++++++
+ 1 file changed, 18 insertions(+)
+
+diff --git a/plugins/imjournal/imjournal.c b/plugins/imjournal/imjournal.c
+--- a/plugins/imjournal/imjournal.c
++++ b/plugins/imjournal/imjournal.c
+@@ -1038,6 +1038,24 @@
+ 		  cs.ratelimitInterval);
+ 	ratelimitSetLinuxLike(ratelimiter, cs.ratelimitInterval, cs.ratelimitBurst);
+ 	ratelimitSetNoTimeCache(ratelimiter);
++	/* The ratelimiter above is module-global, but imjournal runs one
++	 * reader thread per journal: every configured input() plus the
++	 * implicit listener that activateCnf() always adds via
++	 * addListner(NULL, ...). They all call ratelimitAddMsg() on it
++	 * concurrently, so it has to be locked.
++	 *
++	 * Without this, $RepeatedMsgReduction activates
++	 * doLastMessageRepeatedNTimes(), which mutates ratelimit->pMsg
++	 * with every lock in ratelimit.c skipped (bThreadSafe == 0). One
++	 * thread can free that message in msgDestruct() while another is
++	 * still dereferencing it, which is a use-after-free and crashes
++	 * rsyslogd with SIGSEGV.
++	 *
++	 * ratelimitSetNoTimeCache() above already ran pthread_mutex_init()
++	 * on the same mutex; the repeat here is harmless because runInput()
++	 * executes before any reader thread is spawned.
++	 */
++	ratelimitSetThreadSafe(ratelimiter);
+ 
+ 	/* handling old "usepidfromsystem" option */
+ 	if (cs.bUseJnlPID != -1) {
+-- 
+2.47.0
+
diff --git a/recipes-extended/rsyslog/rsyslog_%.bbappend b/recipes-extended/rsyslog/rsyslog_%.bbappend
index da5568a..142635a 100644
--- a/recipes-extended/rsyslog/rsyslog_%.bbappend
+++ b/recipes-extended/rsyslog/rsyslog_%.bbappend
@@ -4,4 +4,5 @@
   file://0001-action.c-reordered-actionDestruc.patch \
   file://0002-imjournal-Fix-invalid-memory-READ-issue.patch \
   file://0003-rsyslog-ossl-Prevent-double-close-of-socket-fd.patch \
+  file://0004-imjournal-make-the-shared-ratelimiter-thread-safe.patch \
 "