Files
spine/util.h
T
Thomas Vincent e8a4b9aac9 fix: poller correctness bugs, with fuzz and integration coverage (#542)
Poller correctness and memory-safety fixes, with the ICMP hardening from
#543 folded in.

Verified in a clean `ubuntu:24.04` container: builds at **38 warnings**
against develop's 39, and the unit tests build and pass on Linux (39
assertions across three binaries).

**Fixes**
- `php_readpipe` off-by-one — the old bound let `bptr` reach
`result_string + 2048` and then wrote the terminator one byte past a
2048-byte allocation.
- `get_namebyhost` used `strtok`, which keeps one process-wide save
pointer, and runs unlocked on every poller thread. Two devices could be
polled at each other's addresses. Now `strtok_r`.
- The log date format was read from `default_dateformat`; Cacti's key is
`default_date_format`, so the lookup never matched and the format was
never loaded.
- `db_query` returns `mysql_store_result()` directly, which is NULL on
store failure. The NULL check ran *after* `mysql_fetch_row`, so the
crash happened one line before the guard.
- `snmp_count` logged a Net-SNMP `errstat` without setting either loop
flag, rebuilding the identical GETNEXT forever and pinning a thread
until the poll interval ended.
- `php_close` skipped the `close()` when the quit write failed with
EPIPE, leaking a descriptor per script-server restart.
- The reap loop's delay was compiled out on `SOLAR_THREAD` builds, so
both phases burned twenty `WNOHANG` polls in nanoseconds and the child
was never reaped.
- ICMP replies are validated for length and matched on id/sequence; on
Cygwin the peeked datagram is now drained, which previously busy-spun
until host timeout.
- The fatal signal handler no longer calls
`malloc`/`localtime_r`/`fprintf` before `_exit` — on a SIGSEGV caused by
heap corruption that deadlocked on the allocator's lock instead of
dying.
- The unit test Makefile was hardcoded to `/opt/homebrew` paths and
`-std=gnu23`, so it could not build on Linux at all. Now pkg-config
driven.

Supersedes #543.

**Added since the first revision**
- `snmp_get_multi()` now checks the varbind type before formatting.
Under v2c an agent reports a per-OID failure as an inline exception
varbind while the PDU `errstat` stays `NOERROR`, so `snprint_value()`
rendered the exception as literal text and stored it as a value.
- Two warning sources removed: a dangling line continuation ending
`sql.h`, and `strncopy()` now using `memcpy` where the byte count is
already exact. Warnings drop from **39 to 7**.

Closes #551
Closes #232
- `read_config_options()` issued 23 separate `SELECT ... FROM settings
WHERE name = ?` round trips at startup; they are now one bulk read
served from memory. Call sites are unchanged.

Closes #330

**Test infrastructure**
- The SNMPv3 integration fixture was broken: the image shipped a binary
that could not start (`libmysqlclient.so.21: cannot open shared object
file`). The build context carried `*.o`, `spine`, `Makefile` and
`config.status`, so `make` treated them as current, skipped compiling,
and packaged a binary linked against libraries the runtime image does
not have. `.dockerignore` now excludes build output. The fixture runs
end to end: spine polls the SNMPv3 device, reads `uptime`, writes to
`poller_output` and exits 0.
- Two libFuzzer targets, run per PR and weekly: the host string parser
and the ICMP reply parser. The second drives
`spine_icmp_classify_reply()`, extracted from `ping_icmp` so the bounds
walk over a shared raw socket is reachable by a test. 15M executions
under ASan/UBSan, no crashes.


**Added in this revision**
- `output_regex` was applied only in the in-loop OID drain, not the pass
after the loop that handles whatever did not fill a `MAX_OIDS` batch, so
a device whose OID count missed a boundary stored raw values. A single
data source is always the leftover case.
`tests/integration/test_output_regex.sh` now asserts on the value: it
fails on the parent commit with the raw reading and passes here with the
filtered one. Addresses #210; the feature stays dormant until Cacti
populates `poller_item.output_regex`.

Merge before #555, which needs the `get_namebyhost` fix above to resolve
a device hostname at all.

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-08-31 10:03:55 +01:00

115 lines
4.7 KiB
C

/*
ex: set tabstop=4 shiftwidth=4 autoindent:*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU Lesser General Public |
| License as published by the Free Software Foundation; either |
| version 2.1 of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Lesser General Public License for more details. |
| |
| You should have received a copy of the GNU Lesser General Public |
| License along with this library; if not, write to the Free Software |
| Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301, USA |
| |
+-------------------------------------------------------------------------+
| spine: a backend data gatherer for cacti |
+-------------------------------------------------------------------------+
| This poller would not have been possible without: |
| - Larry Adams (current development and enhancements) |
| - Rivo Nurges (rrd support, mysql poller cache, misc functions) |
| - RTG (core poller code, pthreads, snmp, autoconf examples) |
| - Brady Alleman/Doug Warner (threading ideas, implementation details) |
+-------------------------------------------------------------------------+
| - Cacti - http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
#ifndef SPINE_UTIL_H
#define SPINE_UTIL_H
/* cacti config reading functions */
extern void read_config_options(void);
extern int read_spine_config(const char *file);
extern void config_defaults(void);
/* cacti logging function */
extern int spine_log(const char *format, ...)
__attribute__((format(printf, 1, 2)));
extern void die(const char *format, ...)
__attribute__((noreturn))
__attribute__((format(printf, 1, 2)));
/* option processing function */
extern void set_option(const char *setting, const char *value);
/* number validation functions */
extern int is_numeric(char *string);
extern int is_ipaddress(const char *string);
extern int all_digits(const char *str);
extern int is_hexadecimal(const char * str, const short ignore_special);
/* determine if a device is a debug device */
extern int is_debug_device(int device_id);
/* string and file functions */
extern char *add_slashes(char *string);
extern int file_exists(const char *filename);
extern char *strip_alpha(char *string);
extern char *strncopy(char *dst, const char *src, size_t n);
extern char *trim(char *str);
extern char *rtrim(char *str);
extern char *ltrim(char *str);
extern char *reverse(char *str);
extern int strpos(const char *haystack, const char *needle);
extern int char_count(const char *str, int chr);
/* custom hex2dec that returns a string instead of a number */
unsigned long long hex2dec(char *str);
/* custom regex replace to return a value if matches */
#define MAX_MATCHES 5
#define REGEX_NUMBER "([-+]*)([0-9]*)([.][0-9]+)"
const char *regex_replace(const char *exp, const char *value);
/* macro to copy string to string with an ending null */
#define STRNCOPY(dst, src) strncopy((dst), (src), sizeof(dst))
#define USTRNCOPY(dst, src) ustrncopy((dst), (src), sizeof(dst))
/* macro to duplicate string and die if fails */
#define STRDUP_OR_DIE(dst, src, reason) \
if ((dst = strdup(src)) == NULL) {\
die("FATAL: malloc() failed during strdup() for %s", reason);\
}\
/* get highres time as double */
extern double get_time_as_double(void);
/* function to check to see if program has capability to use raw socket with
out uid = 0 */
extern int hasCaps(void);
/* see if we can do things as root */
extern void checkAsRoot(void);
/* log format */
extern char *get_date_format(void);
/* remote/main server synchronization */
extern void poller_push_data_to_main(void);
/* start time for spine */
extern double start_time;
/* the version of Cacti as a decimal */
int get_cacti_version(MYSQL *psql, int mode);
#endif /* SPINE_UTIL_H */