Skip to content

Commit ae1befd

Browse files
authored
Merge pull request ossec#2472 from atomicturtle/fix/ant-2026-2bepxi8w-syslog-tcp-framing
Forward every complete line from a syslog-over-TCP recv()
2 parents 3c03f21 + ec7e2ea commit ae1befd

7 files changed

Lines changed: 342 additions & 60 deletions

File tree

‎.github/workflows/make-multi-platform.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ jobs:
121121
./src/issue_739qmecr_readsecmsg_uncompress
122122
make -C src -f tests/regressions/Makefile issue_qy1shlvv_syslog_tcp_idle
123123
./src/issue_qy1shlvv_syslog_tcp_idle
124+
make -C src -f tests/regressions/Makefile issue_2bepxi8w_syslog_tcp_frame
125+
./src/issue_2bepxi8w_syslog_tcp_frame
124126
make -C src -f tests/regressions/Makefile issue_2nlm3gpl_crypto_method_auth
125127
./src/issue_2nlm3gpl_crypto_method_auth
126128
make -C src -f tests/regressions/Makefile issue_kzajsoet_syscheck_fifo

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Unrelease 4.5.0 changes
2929

3030
**Security Fixes**
3131

32+
- @atomicturtle - Forward every complete line from a syslog-over-TCP ``recv()``. ``ossec-remoted`` cut at the first newline and parked the rest without scanning again, so lines that shared a segment were glued onto the next segment as one event, or dropped on close, CRLF, or a full buffer. The worker now appends into the incomplete prefix and drains every complete line before the next read (ANT-2026-2BEPXI8W)
3233
- @atomicturtle - Stop daily reports from leaking per alert in ``ossec-monitord``. ``os_ReportdStart()`` left the ``OS_StrBreak()`` group array and the top-entry list nodes behind; with reports running in a thread that memory stayed for the life of the daemon. The group array is freed after the loop, and each top-entry list's nodes are released with the hash table (covered with ANT-2026-AVE66YB6) (ANT-2026-M3W3LJEQ)
3334
- @atomicturtle - Bound every plain SMTP socket in ``ossec-maild`` and ``ossec-monitord`` to 60 seconds per reply or write. ``OS_Sendmail()`` read through a bare ``recv()`` with no timeout, so a relay that accepted and stayed silent held a sender thread forever. After ``maild.max_workers`` such threads, no further alert e-mail was sent until ``ossec-maild`` was restarted. A timed-out read fails like any other failed reply and releases the worker (ANT-2026-FR6DKH31)
3435
- @atomicturtle - Keep a timed-out active response listed until its ``delete`` has exited. ``ossec-execd`` forked ``delete`` and dropped the entry at once, so the next alert for that address could run ``add`` while ``delete`` was still running. A set-style target such as ``hosts.deny`` or a PF table then lost the block, and later alerts skipped the script because execd still listed the address. A request for that same script and address now waits for the in-flight ``delete`` and is then a new block. A request for a different address does not wait (ANT-2026-8WBHQ0QF)

‎src/remoted/remoted.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ int remoted_send_syslog_msg(remoted_listener *listener, const char *msg,
9696
*/
9797
int syslog_tcp_should_close_on_recv(int r_sz, int err, int pool_should_drop);
9898

99+
/* Drain every complete TCP syslog line from storage_buffer, strip CR and
100+
* leading <PRI>, and keep any incomplete tail at the front. send_fn returns
101+
* <0 to stop. Returns 0 on success or -1 if send_fn failed.
102+
* ANT-2026-2BEPXI8W.
103+
*/
104+
int syslog_tcp_drain_complete_lines(char *storage_buffer, size_t *sb_used,
105+
int (*send_fn)(const char *msg, void *ctx),
106+
void *ctx);
107+
99108
/* Rate limit logging of input rejected before authentication.
100109
* Returns 1 if the caller should log the rejection, 0 if it was only counted.
101110
* Uses remoted_self: call from the listener thread only.

‎src/remoted/syslog_tcp_frame.c‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/* Copyright (C) 2026 Atomicorp, Inc.
2+
* All rights reserved.
3+
*
4+
* This program is a free software; you can redistribute it
5+
* and/or modify it under the terms of the GNU General Public
6+
* License (version 2) as published by the FSF - Free Software
7+
* Foundation.
8+
*/
9+
10+
#include "shared.h"
11+
#include "remoted.h"
12+
13+
#include <string.h>
14+
15+
/* Forward every complete TCP syslog line in storage_buffer. Strip a trailing
16+
* CR and a leading <PRI> on each line, leave any incomplete rest at the front,
17+
* and update *sb_used. send_fn returns <0 to abort. ANT-2026-2BEPXI8W.
18+
*/
19+
int syslog_tcp_drain_complete_lines(char *storage_buffer, size_t *sb_used,
20+
int (*send_fn)(const char *msg, void *ctx),
21+
void *ctx)
22+
{
23+
char *line;
24+
char *line_end;
25+
char *buffer_pt;
26+
27+
if (!storage_buffer || !sb_used || !send_fn) {
28+
return (-1);
29+
}
30+
31+
line = storage_buffer;
32+
while ((line_end = strchr(line, '\n')) != NULL) {
33+
*line_end = '\0';
34+
35+
buffer_pt = strchr(line, '\r');
36+
if (buffer_pt) {
37+
*buffer_pt = '\0';
38+
}
39+
40+
if (line[0] == '<') {
41+
buffer_pt = strchr(line + 1, '>');
42+
if (buffer_pt) {
43+
buffer_pt++;
44+
} else {
45+
buffer_pt = line;
46+
}
47+
} else {
48+
buffer_pt = line;
49+
}
50+
51+
if (send_fn(buffer_pt, ctx) < 0) {
52+
return (-1);
53+
}
54+
55+
line = line_end + 1;
56+
}
57+
58+
*sb_used = strlen(line);
59+
memmove(storage_buffer, line, *sb_used + 1);
60+
return (0);
61+
}

‎src/remoted/syslogtcp.c‎

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -111,39 +111,58 @@ static void syslog_client_drop_pending(void *arg)
111111
free(client);
112112
}
113113

114+
typedef struct {
115+
remoted_listener *listener;
116+
const char *srcip;
117+
} syslog_send_ctx;
118+
119+
static int syslog_worker_send(const char *msg, void *ctx)
120+
{
121+
syslog_send_ctx *send_ctx = (syslog_send_ctx *)ctx;
122+
123+
return remoted_send_syslog_msg(send_ctx->listener, msg, send_ctx->srcip);
124+
}
125+
114126
/* Handle each client connection in a worker thread */
115127
static void *syslog_client_worker(void *arg)
116128
{
117129
syslog_client_arg *client = (syslog_client_arg *)arg;
118130
remoted_listener *listener = client->listener;
119131
int client_socket = client->client_socket;
120132
char srcip[IPSIZE + 1];
121-
int sb_size = OS_MAXSTR;
133+
size_t sb_used = 0;
122134
int r_sz = 0;
135+
syslog_send_ctx send_ctx;
123136

124-
char buffer[OS_MAXSTR + 2];
125137
char storage_buffer[OS_MAXSTR + 2];
126-
char tmp_buffer[OS_MAXSTR + 2];
127-
128-
char *buffer_pt = NULL;
129138

130139
strncpy(srcip, client->srcip, IPSIZE);
131140
srcip[IPSIZE] = '\0';
132141
free(client);
133142

143+
send_ctx.listener = listener;
144+
send_ctx.srcip = srcip;
145+
134146
os_block_worker_signals();
135147

136-
memset(buffer, '\0', OS_MAXSTR + 2);
137148
memset(storage_buffer, '\0', OS_MAXSTR + 2);
138-
memset(tmp_buffer, '\0', OS_MAXSTR + 2);
139149

140150
while (1) {
141151
if (remoted_shutting_down) {
142152
close(client_socket);
143153
return NULL;
144154
}
145155

146-
if ((r_sz = syslog_recv_buffer(client_socket, buffer, OS_MAXSTR - 2)) <= 0) {
156+
/* A line that fills the whole buffer without a newline is dropped */
157+
if (sb_used >= OS_MAXSTR - 2) {
158+
merror("%s: Full buffer receiving from: '%s'", ARGV0, srcip);
159+
sb_used = 0;
160+
storage_buffer[0] = '\0';
161+
}
162+
163+
/* Append behind the incomplete line kept from the previous recv() */
164+
if ((r_sz = syslog_recv_buffer(client_socket, storage_buffer + sb_used,
165+
OS_MAXSTR - (int)sb_used)) <= 0) {
147166
if (remoted_shutting_down) {
148167
close(client_socket);
149168
return NULL;
@@ -170,65 +189,24 @@ static void *syslog_client_worker(void *arg)
170189
return NULL;
171190
}
172191

173-
buffer_pt = strchr(buffer, '\n');
174-
if (!buffer_pt) {
175-
if ((sb_size - r_sz) <= 2) {
176-
merror("%s: Full buffer receiving from: '%s'", ARGV0, srcip);
177-
sb_size = OS_MAXSTR;
178-
storage_buffer[0] = '\0';
179-
continue;
180-
}
192+
/* Map embedded NULs to spaces so strchr/strlen framing cannot drop
193+
* later complete lines (same as read_syslog / ANT-2026-RNCE3SO7). */
194+
{
195+
int i;
181196

182-
strncat(storage_buffer, buffer, sb_size);
183-
sb_size -= r_sz;
184-
continue;
185-
}
186-
187-
if (*(buffer_pt + 1) != '\0') {
188-
*buffer_pt = '\0';
189-
buffer_pt++;
190-
strncpy(tmp_buffer, buffer_pt, OS_MAXSTR);
191-
}
192-
193-
if ((sb_size - r_sz) <= 2) {
194-
merror("%s: Full buffer receiving from: '%s'.", ARGV0, srcip);
195-
sb_size = OS_MAXSTR;
196-
storage_buffer[0] = '\0';
197-
tmp_buffer[0] = '\0';
198-
continue;
199-
}
200-
201-
strncat(storage_buffer, buffer, sb_size);
202-
203-
buffer_pt = strchr(storage_buffer, '\r');
204-
if (buffer_pt) {
205-
*buffer_pt = '\0';
206-
}
207-
208-
if (storage_buffer[0] == '<') {
209-
buffer_pt = strchr(storage_buffer + 1, '>');
210-
if (buffer_pt) {
211-
buffer_pt++;
212-
} else {
213-
buffer_pt = storage_buffer;
197+
for (i = 0; i < r_sz; i++) {
198+
if (storage_buffer[sb_used + (size_t)i] == '\0') {
199+
storage_buffer[sb_used + (size_t)i] = ' ';
200+
}
214201
}
215-
} else {
216-
buffer_pt = storage_buffer;
217202
}
203+
sb_used += (size_t)r_sz;
218204

219-
if (remoted_send_syslog_msg(listener, buffer_pt, srcip) < 0) {
205+
if (syslog_tcp_drain_complete_lines(storage_buffer, &sb_used,
206+
syslog_worker_send, &send_ctx) < 0) {
220207
close(client_socket);
221208
return NULL;
222209
}
223-
224-
if (tmp_buffer[0] != '\0') {
225-
strncpy(storage_buffer, tmp_buffer, OS_MAXSTR);
226-
sb_size = OS_MAXSTR - (strlen(storage_buffer) + 1);
227-
tmp_buffer[0] = '\0';
228-
} else {
229-
storage_buffer[0] = '\0';
230-
sb_size = OS_MAXSTR;
231-
}
232210
}
233211
}
234212

‎src/tests/regressions/Makefile‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ REGRESSION_BINS = \
2828
issue_fbj3la25_rids_counter_reload \
2929
issue_ja2nxew1_reject_log \
3030
issue_qy1shlvv_syslog_tcp_idle \
31+
issue_2bepxi8w_syslog_tcp_frame \
3132
issue_1611_local_ip_bind \
3233
issue_1668_agent_hostname \
3334
issue_1668_ossecalert_hostname \
@@ -206,6 +207,16 @@ issue_qy1shlvv_syslog_tcp_idle: tests/regressions/issue_qy1shlvv_syslog_tcp_idle
206207
-Wl,--start-group shared.a os_regex.a -Wl,--end-group \
207208
-lpcre2-8 -lm -lpthread -lssl -lcrypto
208209

210+
# ANT-2026-2BEPXI8W: TCP syslog must drain every complete line per recv.
211+
issue_2bepxi8w_syslog_tcp_frame: \
212+
tests/regressions/issue_2bepxi8w_syslog_tcp_frame.c \
213+
remoted/syslog_tcp_frame.c shared.a
214+
$(CC) $(CFLAGS) -I./remoted -o $@ \
215+
tests/regressions/issue_2bepxi8w_syslog_tcp_frame.c \
216+
remoted/syslog_tcp_frame.c \
217+
-Wl,--start-group shared.a os_regex.a -Wl,--end-group \
218+
-lpcre2-8 -lm -lpthread -lssl -lcrypto
219+
209220

210221
issue_1611_local_ip_bind: tests/regressions/issue_1611_local_ip_bind.c os_net.a shared.a
211222
$(CC) $(CFLAGS) -o $@ tests/regressions/issue_1611_local_ip_bind.c \

0 commit comments

Comments
 (0)