Merge pull request #7114 from rsyslog/codex/fix-imudp-listenportfilename-vulnerability

imudp: harden listenPortFileName writes
This commit is contained in:
Rainer Gerhards 2026-05-29 14:32:58 +02:00 committed by GitHub
commit d05585d712
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 100 additions and 12 deletions

View File

@ -38,7 +38,10 @@ with ``listenPortFileName`` because one file would be ambiguous. Set
with separate ``listenPortFileName`` values.
When used with a nonzero single port, rsyslog writes the actual bound port
number to the file after the bind succeeds.
number to the file after the bind succeeds. The file is created with owner-only
permissions when it does not already exist, and rsyslog refuses symlinks, FIFOs,
and other special files. Configure this path in a trusted directory such as
``/run/rsyslog`` rather than a directory writable by unprivileged users.
Input usage
-----------
@ -47,7 +50,7 @@ Input usage
.. code-block:: rsyslog
input(type="imudp" port="0" listenPortFileName="/tmp/imudp.port")
input(type="imudp" port="0" listenPortFileName="/run/rsyslog/imudp.port")
See also
--------

View File

@ -30,8 +30,10 @@
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <pthread.h>
#include <signal.h>
#include <poll.h>
@ -262,34 +264,82 @@ finalize_it:
}
static rsRetVal writeListenPortFile(const uchar *const pszLstnPortFileName, const unsigned listenPort) {
FILE *fp = NULL;
char portBuf[32];
const char *const path = (const char *)pszLstnPortFileName;
struct stat st;
ssize_t len;
ssize_t done = 0;
int fd = -1;
DEFiRet;
if ((fp = fopen((const char *)pszLstnPortFileName, "w")) == NULL) {
const int lstatRet = lstat(path, &st);
if (lstatRet == 0 && !S_ISREG(st.st_mode)) {
LogError(0, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"refusing to write to non-regular file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
} else if (lstatRet != 0 && errno != ENOENT) {
LogError(errno, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"error while trying to inspect file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
}
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK, S_IRUSR | S_IWUSR);
if (fd == -1) {
LogError(errno, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"error while trying to open file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
}
if (fprintf(fp, "%u", listenPort) < 0) {
if (fstat(fd, &st) != 0) {
LogError(errno, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"error while trying to write file");
fclose(fp);
fp = NULL;
"error while trying to inspect open file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
}
if (fclose(fp) != 0) {
fp = NULL;
if (!S_ISREG(st.st_mode)) {
LogError(0, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"refusing to write to non-regular file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
}
len = snprintf(portBuf, sizeof(portBuf), "%u", listenPort);
portBuf[sizeof(portBuf) - 1] = '\0';
if (len < 0 || len >= (ssize_t)sizeof(portBuf)) {
LogError(0, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"port string truncated or encoding error");
ABORT_FINALIZE(RS_RET_IO_ERROR);
}
while (done < len) {
const ssize_t written = write(fd, portBuf + done, (size_t)(len - done));
if (written < 0) {
if (errno == EINTR) continue;
LogError(errno, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"error while trying to write file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
} else if (written == 0) {
LogError(0, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"error while trying to write file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
}
done += written;
}
if (close(fd) != 0) {
fd = -1;
LogError(errno, RS_RET_IO_ERROR,
"imudp: listenPortFileName: "
"error while trying to close file");
ABORT_FINALIZE(RS_RET_IO_ERROR);
}
fp = NULL;
fd = -1;
finalize_it:
if (fp != NULL) fclose(fp);
if (fd != -1) close(fd);
RETiRet;
}

View File

@ -265,6 +265,7 @@ TESTS_DEFAULT = \
sndrcv_udp_nonstdpt.sh \
sndrcv_udp_nonstdpt_v6.sh \
imudp_thread_hang.sh \
imudp-listenportfilename-secure.sh \
imudp_ratelimit_name.sh \
omfwd_ratelimit_name.sh \
omelasticsearch_ratelimit_name.sh \

View File

@ -0,0 +1,34 @@
#!/bin/bash
# Regression test for imudp listenPortFileName path handling. The listener
# writes its bound port before privilege drop, so the oracle is an internal
# diagnostic that rejects a symlink handoff path plus an unchanged symlink
# target. The timeout only bounds the foreground daemon after config activation.
. ${srcdir:=.}/diag.sh init
TARGET_FILE="${RSYSLOG_DYNNAME}.target"
PORT_FILE="${RSYSLOG_DYNNAME}.imudp_port"
printf 'must-stay-intact\n' > "$TARGET_FILE"
ln -s "$TARGET_FILE" "$PORT_FILE"
generate_conf
add_conf '
module(load="../plugins/imudp/.libs/imudp")
input(type="imudp" address="127.0.0.1" port="0" listenPortFileName="'$PORT_FILE'")
'
startup_common
set +e
timeout 3 ../tools/rsyslogd -C -n -i"${RSYSLOG_PIDBASE}.pid" -M"$RSYSLOG_MODDIR" -f"$CONF_FILE" \
>"${RSYSLOG_DYNNAME}.startup.log" 2>&1
rc=$?
set -e
if [ "$rc" -eq 0 ]; then
echo "FAIL: expected foreground rsyslogd to keep running or report an error after activation, rc=$rc"
cat "${RSYSLOG_DYNNAME}.startup.log"
error_exit 1
fi
printf 'must-stay-intact\n' > "${RSYSLOG_DYNNAME}.expected"
cmp_exact_file "${RSYSLOG_DYNNAME}.expected" "$TARGET_FILE"
content_check "listenPortFileName: refusing to write to non-regular file" "${RSYSLOG_DYNNAME}.startup.log"
exit_test