Skip to content

Commit afc2b71

Browse files
authored
Merge pull request ossec#2462 from atomicturtle/fix/ant-2026-9knxmd94-wildcard-cap
Cap how many files one wildcard location opens
2 parents b7c3e8b + 8c2d405 commit afc2b71

13 files changed

Lines changed: 281 additions & 0 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ jobs:
9191
./src/issue_nceh7rb6_json_rotate_flag
9292
make -C src -f tests/regressions/Makefile issue_ygx1kkkt_dated_log_tail
9393
./src/issue_ygx1kkkt_dated_log_tail
94+
make -C src -f tests/regressions/Makefile issue_9knxmd94_wildcard_cap
95+
./src/issue_9knxmd94_wildcard_cap
9496
make -C src -f tests/regressions/Makefile issue_rxknzj1n_smtp_dot_escape
9597
./src/issue_rxknzj1n_smtp_dot_escape
9698
make -C src -f tests/regressions/Makefile issue_fbj3la25_rids_counter_reload

‎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 - Stop one wildcard ``<location>`` from opening every match. ``logcollector.max_wildcard_files`` (default 256) keeps the first matches and logs the rest. ``ossec-logcollector`` raises its open-file limit to the number of configured files plus 128, so several wildcard blocks still leave room for later logs, commands, and ``ossec.log``. A pattern that already matches more than 256 files monitors only the first 256 until the option is raised (ANT-2026-9KNXMD94)
3233
- @atomicturtle - Read a date-based log to its current end before the expanded name changes. ``update_fname()`` was closing the old file at the start of the poll, so lines written in the last ``loop_timeout`` never reached ``ossec-analysisd``. The lines are read while the location is still the old name. A name that has not changed is left alone (ANT-2026-YGX1KKKT)
3334
- @atomicturtle - Forward the first JSON ``syslog_output`` alert written after ``alerts.json`` rotates or is truncated. ``jqueue_next()`` left ``JQUEUE_READ_FAILED`` set when the new file was still empty, and the next call parsed a good alert and deleted it. The alert stayed in ``alerts.json`` and still drove mail, the database, and active response (ANT-2026-NCEH7RB6)
3435
- @atomicturtle - Keep real ``syslog_output`` fields whose text happens to look like a placeholder. The default, CEF, and Splunk formatters dropped a value when a single character lined up with ``(none)``, ``(no user)``, or ``unknown``, so a user named ``anna``, a host ``inn.example.org``, or a location starting with ``(n`` never reached the receiver. Only the exact strings ``(none)`` and ``(no user)`` are left out. A user named ``unknown`` is still forwarded (ANT-2026-9Q8LR4NY)

‎etc/internal_options.conf‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ logcollector.open_attempts=8
9090
# before the other files are looked at [0..1000000]. 0 disables the limit.
9191
logcollector.max_lines=10000
9292

93+
# Maximum files one wildcard <location> may monitor [1..100000].
94+
# Extra matches are not opened. Raise this, and the process file limit
95+
# follows, when one pattern legitimately matches more than 256 files.
96+
logcollector.max_wildcard_files=256
97+
9398
# Logcollector - If it should accept remote commands from the manager
9499
logcollector.remote_commands=0
95100

‎src/config/localfile-config.c‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ static void init_empty_logreader(logreader *lr)
1818
lr->ign = 360;
1919
}
2020

21+
/* One wildcard must not add an unbounded number of open files.
22+
* Unset means the shipped default. */
23+
static int wildcard_file_cap(const logreader_config *log_config)
24+
{
25+
if (!log_config || log_config->max_wildcard_files <= 0) {
26+
return 256;
27+
}
28+
return log_config->max_wildcard_files;
29+
}
30+
2131
#ifdef WIN32
2232
/* True if path has a real * / ? wildcard (ignore '?' in a \\?\ prefix). */
2333
static int localfile_has_win_wildcard(const char *path)
@@ -223,7 +233,16 @@ int Read_Localfile(XML_NODE node, void *d1, __attribute__((unused)) void *d2)
223233
glob_set = pl + 1;
224234
}
225235

236+
int cap = wildcard_file_cap(log_config);
237+
226238
for (g = 0; paths[g] != NULL; g++) {
239+
if (g >= cap) {
240+
merror(GLOB_MAXFILES, __local_name, node[i]->content, cap);
241+
for (; paths[g] != NULL; g++) {
242+
free(paths[g]);
243+
}
244+
break;
245+
}
227246
/* Expanded names are real files. A '%' in the filename
228247
* is literal (for example 100%done.log), not a strftime
229248
* template. The template lives on the configured location.
@@ -273,7 +292,14 @@ int Read_Localfile(XML_NODE node, void *d1, __attribute__((unused)) void *d2)
273292
}
274293

275294

295+
int cap = wildcard_file_cap(log_config);
296+
276297
while (g.gl_pathv[glob_offset] != NULL) {
298+
if (glob_offset >= cap) {
299+
merror(GLOB_MAXFILES, __local_name, node[i]->content, cap);
300+
break;
301+
}
302+
277303
/* Check for strftime on globs too */
278304
if (strchr(g.gl_pathv[glob_offset], '%')) {
279305
struct tm *p;

‎src/config/localfile-config.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ typedef struct _logreader {
5555
typedef struct _logreader_config {
5656
int agent_cfg;
5757
int accept_remote;
58+
/* Max names one wildcard <location> may add. 0 means unset. */
59+
int max_wildcard_files;
5860
logreader *config;
5961
} logreader_config;
6062

‎src/error_messages/error_messages.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
//#define PIPE_ERROR "%s(1120): ERROR: Pipe error."
3636
#define GLOB_ERROR "%s(1121): ERROR: Glob error. Invalid pattern: '%s'."
3737
#define GLOB_NFOUND "%s(1122): ERROR: No file found by pattern: '%s'."
38+
#define GLOB_MAXFILES "%s(1954): ERROR: Too many files found by pattern: '%s'. Only the first %d are monitored (logcollector.max_wildcard_files)."
3839
#define UNLINK_ERROR "%s(1123): ERROR: Unable to delete file: '%s'."
3940
#define RENAME_ERROR "%s(1124): ERROR: Could not rename file '%s' to '%s' due to [(%d)-(%s)]."
4041
#define INT_ERROR "%s(1125): ERROR: Internal error (undefined)."

‎src/logcollector/config.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ int LogCollectorConfig(const char *cfgfile, int accept_remote)
2222
log_config.config = NULL;
2323
log_config.agent_cfg = 0;
2424
log_config.accept_remote = accept_remote;
25+
log_config.max_wildcard_files = getDefine_Int("logcollector",
26+
"max_wildcard_files", 1, 100000);
2527

2628
if (ReadConfig(modules, cfgfile, &log_config, NULL) < 0) {
2729
return (OS_INVALID);

‎src/logcollector/logcollector.c‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
#include "shared.h"
1111
#include "logcollector.h"
1212

13+
#ifndef WIN32
14+
#include <sys/resource.h>
15+
#endif
16+
1317
/* Prototypes */
1418
int update_fname(int i);
1519
static void clear_logreader_entry(int idx);
@@ -748,6 +752,54 @@ int update_fname(int i)
748752
return (0);
749753
}
750754

755+
/* Raise the soft open-file limit so the files this process will keep
756+
* open, plus the queue, the log, and command pipes, fit. One wildcard is
757+
* already capped; several wildcard blocks can still pass that cap.
758+
*/
759+
void logcollector_raise_nofile(int nfiles)
760+
{
761+
#ifndef WIN32
762+
struct rlimit rl;
763+
rlim_t need;
764+
rlim_t hard;
765+
766+
if (nfiles < 0) {
767+
nfiles = 0;
768+
}
769+
need = (rlim_t)nfiles + 128;
770+
if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
771+
return;
772+
}
773+
if (rl.rlim_cur >= need) {
774+
return;
775+
}
776+
777+
hard = rl.rlim_max;
778+
if (hard < need) {
779+
rl.rlim_max = need;
780+
}
781+
rl.rlim_cur = need;
782+
if (setrlimit(RLIMIT_NOFILE, &rl) == 0) {
783+
return;
784+
}
785+
786+
/* Stay inside the hard limit this process is allowed to use. */
787+
if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
788+
return;
789+
}
790+
if (rl.rlim_cur < hard) {
791+
rl.rlim_cur = hard;
792+
if (setrlimit(RLIMIT_NOFILE, &rl) == 0) {
793+
return;
794+
}
795+
}
796+
merror("%s: WARN: Unable to raise the open-file limit to %llu (%s).",
797+
ARGV0, (unsigned long long)need, strerror(errno));
798+
#else
799+
(void)nfiles;
800+
#endif
801+
}
802+
751803
/* Open, get the fileno, seek to the end and update mtime */
752804
int handle_file(int i, int do_fseek, int do_log)
753805
{

‎src/logcollector/logcollector.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ void LogCollectorStart(void) __attribute__((noreturn));
2929
/* Handle files */
3030
int handle_file(int i, int do_fseek, int do_log);
3131

32+
/* Raise RLIMIT_NOFILE to cover nfiles plus a reserve for the queue and log. */
33+
void logcollector_raise_nofile(int nfiles);
34+
3235
/* Per-visit line budget for file readers (logcollector.max_lines). */
3336
void logr_begin_read(void);
3437
int logr_consume_line(int drop_it);

‎src/logcollector/main.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ int main(int argc, char **argv)
137137
logff[1].logformat = NULL;
138138

139139
merror(NO_FILE, ARGV0);
140+
} else {
141+
int nfiles = 0;
142+
143+
while (logff[nfiles].file || logff[nfiles].command) {
144+
nfiles++;
145+
}
146+
logcollector_raise_nofile(nfiles);
140147
}
141148

142149
/* Start signal handler */

0 commit comments

Comments
 (0)