Skip to content

Fix a batch of unaddressed bugs (#594, #600, #605, #607, #575, #583, #580) - #627

Merged
TheWitness merged 6 commits into
developfrom
bugfix-batch-develop
Sep 22, 2026
Merged

TheWitness merged 6 commits into
developfrom
bugfix-batch-develop

Conversation

@TheWitness

@TheWitness TheWitness commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Summary

A batch of small, independent, unaddressed bugs from the open-issue backlog. None of these overlapped with any currently open PR's actual diff (verified by inspecting each candidate PR's changes directly, not just its declared Closes list).

Fixes

  • poll_host() skips mysql_thread_end() when the device row is missing — the early return on num_rows != 1 released both DB connections and freed six allocations, but never called mysql_thread_end(), unlike the other two exit paths. Added the missing call.
  • SNMPv3 passphrase scrubbing flag never set — zero_sensitive was a function-local initialized to 0 and never assigned, so every guarded memset() was dead code. Made scrubbing of this module's own heap copies (Apsz/Xpsz) unconditional, and moved the wipe to immediately precede each free (including both generate_Ku() error paths) after review found the first pass still scrubbed before assignment. Left the caller's snmp_password/snmp_priv_passphrase alone rather than also scrubbing those in place — those are host_t-owned and get reused for later session rebuilds against the same host, so zeroing them would have broken subsequent polls.
  • php_readpipe() FD_SET without an FD_SETSIZE check — FD_SET() doesn't range-check, so a read descriptor at or above FD_SETSIZE (1024 on glibc) wrote past the end of the on-stack fd_set. Added a bounds check; on an out-of-range descriptor it marks the result undefined and closes the slot (a review finding caught the first version recursing without bound through php_init() → php_readpipe() under sustained descriptor exhaustion, so it no longer restarts synchronously from here).
  • Signal handler never re-arms, so the second SIGPIPE kills the poller — every fatal signal reset itself to SIG_DFL and never re-armed. That's the right idiom for a real fault (SIGSEGV and friends re-execute and die with a core), but SIGPIPE is a routine, recurring condition — a second one previously killed the whole poller. A review finding pointed out that re-arming only at the end still left a window (signal dispositions are process-wide) where another thread's SIGPIPE could hit SIG_DFL; fixed by never resetting SIGPIPE's disposition in the first place.
  • spine fails to link on Fedora: undefined reference to usmDESPrivProtocol — some distributions ship a net-snmp-config.h that advertises DES as the default privacy protocol while the shipped libnetsnmp doesn't export the symbol. Added an AC_LINK_IFELSE probe for the actual symbol and guarded the fallback to use usmAESPrivProtocol when it's absent.
  • Poller results ≥1024 bytes silently truncated to 1022 — db_escape() capped its internal trim buffer at a fixed DBL_BUFSIZE regardless of the caller's max_size, so raising --with-results-buffer never helped. The trim buffer now scales with max_size (a review finding caught the trim-size formula going negative/zero for max_size 1-3), and escaped_result in poll_host() is sized to RESULTS_BUFFER * 2 + 1 instead of the fixed DBL_BUFSIZE. (A very large escaped result can still hit the separate, already-tracked row-size limit in poller_output rows are truncated when the escaped result exceeds 2304 bytes #598/ci: enforce cross-platform quality gates and connector coverage #618 — this fix only addresses the escaping-stage truncation.)

Also included

  • Seed spine.conf from spine.conf.dist on first install — make install only ever installed spine.conf.dist; nothing produced a live spine.conf. Added an install-data-hook that copies it over only when spine.conf doesn't already exist, plus a matching uninstall-hook that only removes it again when it's still byte-identical to the .dist (so a real uninstall never discards a customized config, while make distcheck's install/uninstall symmetry check still passes).

Dropped from this PR

  • A custom codeql.yml workflow (originally added for CodeQL stopped running on develop after #452 #580) was removed — the repo already has GitHub's default CodeQL setup enabled and running (gh api repos/Cacti/spine/code-scanning/default-setup → state: configured), and a custom "advanced configuration" workflow cannot coexist with it; GitHub rejected the SARIF upload and failed CI. CodeQL stopped running on develop after #452 #580's underlying gap was already resolved out of band, so this PR no longer claims to close it.

Testing

Full build via WSL (./bootstrap && ./configure --enable-warnings && make) succeeds with no new warnings in poller.c, snmp.c, php.c, error.c, or sql.c beyond two pre-existing, unrelated -Wformat-truncation warnings in php_init(). Confirmed the usmDESPrivProtocol configure probe correctly detects the missing symbol in this build environment and takes the AES fallback path without a link error. make distcheck passes end to end.

Closes #594
Closes #600
Closes #605
Closes #607
Closes #575
Closes #583

…scrub, FD_SETSIZE, SIGPIPE, DES link, escape sizing, CodeQL

poll_host(): call mysql_thread_end() on the device-row-missing exit path, matching the other two exits (#594).

snmp_host_init(): the zero_sensitive scrub flag was always 0, so the guarded memset() calls that were supposed to clear SNMPv3 passphrase copies never ran. Make scrubbing of this module's own heap copies (Apsz/Xpsz) unconditional; drop the guarded memsets against the caller's snmp_password/snmp_priv_passphrase, since those are host_t-owned and reused for later session rebuilds on the same host (#600).

php_readpipe(): FD_SET() does not range-check, so a read descriptor at or above FD_SETSIZE wrote past the end of the on-stack fd_set. Added a bounds check that treats an out-of-range descriptor as a script-server failure and restarts it (#605).

spine_signal_handler(): every fatal signal reset itself to SIG_DFL and never re-armed. That is the correct idiom for a real fault (SIGSEGV and friends re-execute and die with a core), but SIGPIPE is a routine, recurring condition; a second one killed the poller outright. Re-arm only SIGPIPE (#607).

configure.ac/snmp.c: some distributions (e.g. Fedora) ship a net-snmp-config.h that advertises usmDESPrivProtocol as the default privacy protocol without libnetsnmp actually exporting the symbol, so spine failed to link. Added an AC_LINK_IFELSE probe for the real symbol and guarded the fallback to use usmAESPrivProtocol when it is absent (#575).

sql.c db_escape() capped its internal trim buffer at a fixed DBL_BUFSIZE regardless of the caller's max_size, so any result of 1024 bytes or more was silently cut to 1022 characters no matter how large --with-results-buffer was set. The trim buffer now scales with max_size, and poller.c's escaped_result destination is sized to match RESULTS_BUFFER instead of the fixed DBL_BUFSIZE (#583).

Restored a CodeQL workflow for develop. It was dropped as an incidental side effect of an unrelated CI change in #452 and nothing replaced it, so develop has had no static analysis coverage since (#580).
Copilot AI lite review requested due to automatic review settings September 22, 2026 13:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical and moderate issues remain in workflow permissions, signal handling, restart behavior, secret scrubbing, and SQL buffer sizing.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity · 2 Medium severity

Open (5)
What changed in this PR

This pull request fixes seven independent bugs across polling, SNMP, portability, SQL escaping, signal handling, and CodeQL CI.

Changes:

  • Fixes cleanup, descriptor bounds, signal handling, and result-buffer sizing.
  • Improves SNMPv3 scrubbing and DES/AES portability.
  • Restores the CodeQL workflow.
File Changes
sql.c Scales escaping buffers with the requested output size.
snmp.c Updates passphrase handling and privacy-protocol fallback.
poller.c Adds MySQL thread cleanup and larger escaped-result storage.
php.c Guards against oversized descriptors.
error.c Re-arms SIGPIPE handling.
configure.ac Probes DES symbol availability.
.github/​workflows/​codeql.yml Restores CodeQL scanning.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/codeql.yml Outdated
Comment thread error.c Outdated
Comment thread sql.c Outdated
Comment thread php.c Outdated
Comment thread snmp.c Outdated
Codifies the existing -issue#NNN: convention so future PRs (including AI-assisted ones) add a CHANGELOG entry referencing the issue they close.
codeql.yml: the job-level permissions block overrode the workflow defaults and dropped contents: read, which checkout needs. Added it back at the job level.

error.c: re-arming SIGPIPE only at the end of the handler still left a window, between the top-of-handler reset and the re-arm, where another thread's broken-pipe write would hit SIG_DFL and terminate the process (signal dispositions are process-wide). Skip the reset entirely for SIGPIPE instead, so the handler stays installed throughout.

sql.c: db_escape()'s max_size guard allowed 1-3, where (max_size/2)-1 became 0 (unterminated output) or -1 (converted to a huge size_t that overflowed the allocation). Reworked the trim size calculation so it always reserves the terminator for any max_size >= 1.

php.c: the new FD_SETSIZE guard called php_close()+php_init() to restart the server, but php_init() validates the replacement by calling back into this function, which can hit the same guard again under sustained descriptor exhaustion and recurse without bound. Close and mark the slot busy instead of restarting synchronously from within the guard; the next independent call attempts recovery.

snmp.c: the zero_sensitive fix scrubbed Apsz/Xpsz before they were assigned, which is always a no-op since both start NULL every call. Moved the memset to immediately precede each of their frees, including the two generate_Ku() error-return paths, so the live passphrase copy is actually cleared before every exit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Moderate unresolved issues remain in SIGPIPE exit-state handling and guaranteed SNMP passphrase wiping.

Review effort: Lite
Findings: None

Resolved since last review (5)

config_DATA only ever installed spine.conf.dist itself; nothing produced a
live spine.conf, so a fresh install had to be copied by hand before spine
would read any configuration. Add an install-data-hook that copies
spine.conf.dist to spine.conf under $(configdir) only when spine.conf does
not already exist, so a first install seeds a usable config and a re-install
never clobbers local edits.

Verified via WSL: a fresh 'make install DESTDIR=...' creates etc/spine.conf
identical to spine.conf.dist; a second install with a hand-edited spine.conf
leaves it untouched.
…etry

- .github/workflows/codeql.yml: the repo already has GitHub's default
  CodeQL setup enabled and running successfully (confirmed via
  `gh api repos/Cacti/spine/code-scanning/default-setup` -> state:
  "configured", covering actions/c-cpp/ruby). A custom "advanced
  configuration" workflow cannot coexist with default setup - GitHub
  rejected the uploaded SARIF with "CodeQL analyses from advanced
  configurations cannot be processed when the default setup is
  enabled", failing the analyze job outright. Removed the redundant
  workflow; issue #580's underlying gap (no scanning on develop) was
  already resolved out of band by enabling default setup, so this PR
  no longer claims to close it.

- Makefile.am: `make distcheck`'s distuninstallcheck step install/
  uninstalls into a sandbox and fails if any file is left behind.
  install-data-hook seeds spine.conf, but nothing removed it again on
  uninstall, so distcheck failed with "files left after uninstall:
  ./etc/spine.conf". Added a matching uninstall-hook that only removes
  spine.conf when it is still byte-identical to spine.conf.dist (i.e.
  never customized) - a real uninstall never discards a live config,
  while distcheck's untouched sandbox copy still gets cleaned up.

Verified via WSL: `make distcheck` now passes end to end.
@TheWitness
TheWitness merged commit 4544dca into develop Sep 22, 2026
13 checks passed
@TheWitness
TheWitness deleted the bugfix-batch-develop branch September 22, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants