Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
c126eb4
fix: restore the close-on-exec and bounded reap in nft_popen
somethingwithproof Sep 3, 2026
8c65c24
fix: check the calloc results in spine.c
somethingwithproof Aug 31, 2026
7fd486a
fix(log): bound the newline appended to a full log message
somethingwithproof Aug 31, 2026
5c4fc4e
fix(util): free the result set on the NULL-row branch
somethingwithproof Aug 31, 2026
e23a472
perf(log): build the timestamp format once at config load
somethingwithproof Aug 31, 2026
30c1535
docs(changelog): record the allocation and log path fixes
somethingwithproof Aug 31, 2026
98a9268
test: cover the changed util.c and spine.c paths
somethingwithproof Sep 1, 2026
86201e7
fix: size the escape destination from the caller's buffer
somethingwithproof Sep 2, 2026
ea71557
fix: restore the SNMPv3 noAuthNoPriv and authNoPriv security levels
somethingwithproof Sep 2, 2026
6840bf1
fix: replace the 'p += snprintf' idiom with a bounded append
somethingwithproof Sep 3, 2026
0fda76e
test: cover the poller result predicates against the shipped objects
somethingwithproof Sep 2, 2026
32cc648
ci: share the Spine build setup through a composite action
somethingwithproof Sep 2, 2026
d0a7247
ci: run the unit tests under ASan and UBSan
somethingwithproof Sep 2, 2026
e3b0e80
refactor: name the poller_item scoping rule that poll_host duplicates
somethingwithproof Sep 2, 2026
d05d4f7
refactor: build poll_host queries once instead of per poller type
somethingwithproof Sep 3, 2026
691587d
fix: apply dbonupdate to the remote poller_output upsert
somethingwithproof Sep 3, 2026
a621539
refactor: make poll_host's query construction a callable unit
somethingwithproof Sep 3, 2026
db488a5
fix: end the MySQL thread on every poll_host exit
somethingwithproof Sep 3, 2026
ca841d7
refactor: name the data query reindex assert
somethingwithproof Sep 3, 2026
c72e4b9
test: reconcile the unit tests across the batch
somethingwithproof Sep 3, 2026
bbbddce
test: reach poll_host's teardown by interposing on its two sinks
somethingwithproof Sep 3, 2026
1dea648
refactor: map a poller_item row in its own function
somethingwithproof Sep 3, 2026
e89a687
test: run the integration and regression suites
somethingwithproof Sep 3, 2026
0b12de4
refactor: format a poller_output row in its own function
somethingwithproof Sep 3, 2026
66fc85a
fix: size the poller_output tuple buffer for a fully escaped row
somethingwithproof Sep 3, 2026
ff2c252
fix: give ping_icmp one owner for its packet and socket
somethingwithproof Sep 3, 2026
a5419b6
refactor: give poll_host one exit and one owner for its allocations
somethingwithproof Sep 3, 2026
896fda0
refactor: normalise a polled result in its own function
somethingwithproof Sep 3, 2026
b5f8806
fix: convert dash and colon separated hex rather than returning zero
somethingwithproof Sep 3, 2026
c54f8d7
refactor: process one SNMP multi-get batch in its own function
somethingwithproof Sep 3, 2026
f6ad8e2
refactor: map a host row in its own function
somethingwithproof Sep 3, 2026
bfef030
test: cover the rejection and logging branches of both normalisers
somethingwithproof Sep 3, 2026
3adbf60
test: reach spine_appendf's formatting-error branch
somethingwithproof Sep 3, 2026
e33a29a
test: verify the two restructured extractions against what they replaced
somethingwithproof Sep 3, 2026
4c32a66
refactor: build the host status UPDATE in its own function
somethingwithproof Sep 3, 2026
dc70e8c
test: require an explicit opt-in for the fixture-backed integration t…
somethingwithproof Sep 3, 2026
36eb5e9
test: skip the allocation-failure injection under AddressSanitizer
somethingwithproof Sep 3, 2026
65cd621
build: probe for usmDESPrivProtocol instead of trusting the macro
somethingwithproof Sep 3, 2026
e0daf94
fix: report why setting close-on-exec failed
somethingwithproof Sep 3, 2026
a778b5b
fix: validate the SNMPv3 auth protocol whenever one is set
somethingwithproof Sep 3, 2026
0a30146
fix: refuse a NULL where the release helpers dereference one
somethingwithproof Sep 3, 2026
a77182d
fix: four defects the pre-push review found in this branch
somethingwithproof Sep 3, 2026
62d98ac
fix: refuse SNMPv3 privacy that has no authentication, and cover the …
somethingwithproof Sep 3, 2026
8d2e740
test: cover ping_icmp, including the exit that leaked
somethingwithproof Sep 3, 2026
416af9f
fix: release the authNoPriv passphrase copy, and make CI run the ping…
somethingwithproof Sep 3, 2026
cacd652
fix: three defects the review found, including one my dedup pass missed
somethingwithproof Sep 3, 2026
b4766cb
fix: two critical defects my own fix introduced in nft_popen
somethingwithproof Sep 3, 2026
1aa49aa
build: probe for linker --wrap rather than assuming it, and tidy thre…
somethingwithproof Sep 3, 2026
d0bda5f
refactor: drop two locals the extraction orphaned, and stop a test pa…
somethingwithproof Sep 3, 2026
14e6e60
fix: php_init closed the script server's stdin or stdout on a descrip…
somethingwithproof Sep 3, 2026
89735f2
test: prove nft_popen releases ListMutex when dup fails
somethingwithproof Sep 3, 2026
6b2a2c9
test: report a skipped test as skipped rather than as a pass
somethingwithproof Sep 3, 2026
18d2b5a
test: assert the reap polls before sleeping instead of timing it
somethingwithproof Sep 3, 2026
fb99e5b
fix: send the portable upsert from the remote push path
somethingwithproof Sep 3, 2026
9ce88fb
fix: release LOCK_SETEUID between ICMP socket attempts
somethingwithproof Sep 3, 2026
81c6a1a
fix: reap the children nft_pclose gives up on
somethingwithproof Sep 3, 2026
58d5021
fix: hold ListMutex uncancellable when parking an abandoned pid
somethingwithproof Sep 3, 2026
ec2ae3d
test: guard the remote upsert rule in the source
somethingwithproof Sep 3, 2026
2348907
fix: flush the remote push batch before the row, not in place of it
somethingwithproof Sep 3, 2026
dea9d04
test: cover the parked-child sweep
somethingwithproof Sep 3, 2026
d0b3a59
test: guard the remote push batching alongside the upsert rule
somethingwithproof Sep 3, 2026
84f0e75
fix: give php_init() one owner for the resources it allocates
somethingwithproof Sep 3, 2026
4d8bc79
fix: clear the caller's descriptors when the cloexec pipe open fails
somethingwithproof Sep 3, 2026
7fe2826
test: guard php_init's single teardown and the pipe helper's contract
somethingwithproof Sep 3, 2026
01c7bf8
fix: serialise the log emit so concurrent messages are not spliced
somethingwithproof Sep 3, 2026
e627c2d
test: reproduce the spliced log line from #298
somethingwithproof Sep 3, 2026
3ff82d3
test: characterize the device availability state machine
somethingwithproof Sep 3, 2026
b35387d
fix: leave hostinfo alone when getaddrinfo() fails
somethingwithproof Sep 3, 2026
6f4d964
fix: give nft_pclose exclusive ownership of the entry it closes
somethingwithproof Sep 3, 2026
30ddd02
fix: bound the script server descriptor before FD_SET
somethingwithproof Sep 3, 2026
a004268
fix: stop php_cmd leaking its undefined-value string on a successful …
somethingwithproof Sep 3, 2026
ebab7dd
fix: ignore SIGPIPE instead of handling it once
somethingwithproof Sep 3, 2026
78bacf9
fix: wipe the local SNMPv3 passphrase copies before freeing them
somethingwithproof Sep 3, 2026
95c6323
fix: bound the script server restart at the startup handshake
somethingwithproof Sep 3, 2026
572ef55
fix: compile REGEX_NUMBER with REG_EXTENDED
somethingwithproof Sep 3, 2026
30d08d8
fix: clear the destination when db_escape() is given a NULL column
somethingwithproof Sep 3, 2026
07c5b47
fix: allocate the details array zeroed
somethingwithproof Sep 3, 2026
8b83a45
fix: terminate error_string before the host_errors insert can read it
somethingwithproof Sep 3, 2026
830ea9d
fix: size last_snmp_community to match target_t
somethingwithproof Sep 3, 2026
a725531
fix: require 8.0.20 before choosing the row alias upsert form
somethingwithproof Sep 3, 2026
b31b333
test: exercise PHP and SQL runtime paths
somethingwithproof Sep 4, 2026
2e56617
fix: close reviewed runtime coverage gaps
somethingwithproof Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/actions/build-spine/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Build Spine
description: Install the build dependencies, then bootstrap and configure Spine.

inputs:
extra-packages:
description: Additional apt packages, beyond the set every Spine build needs.
required: false
default: ''
configure-args:
description: Arguments passed through to ./configure.
required: false
default: ''
cflags:
description: CFLAGS for ./configure.
required: false
default: ''
ldflags:
description: LDFLAGS for ./configure.
required: false
default: ''

runs:
using: composite
steps:
# mysql-server is deliberately absent: configure only looks for mysql.h and
# libmysqlclient, both of which come from libmysqlclient-dev, and no job
# starts or connects to a server.
- name: Install build dependencies
shell: bash
env:
EXTRA_PACKAGES: ${{ inputs.extra-packages }}
run: |
set -euo pipefail
sudo apt-get update
# shellcheck disable=SC2086
sudo apt-get install -y \
autoconf automake libtool build-essential help2man dos2unix \
libmysqlclient-dev libsnmp-dev libssl-dev libcmocka-dev \
$EXTRA_PACKAGES

- name: Bootstrap and configure
shell: bash
env:
CONFIGURE_ARGS: ${{ inputs.configure-args }}
IN_CFLAGS: ${{ inputs.cflags }}
IN_LDFLAGS: ${{ inputs.ldflags }}
run: |
set -euo pipefail
./bootstrap
args=()
if [ -n "$CONFIGURE_ARGS" ]; then
read -ra args <<< "$CONFIGURE_ARGS"
fi
if [ -n "$IN_CFLAGS" ]; then
args+=("CFLAGS=$IN_CFLAGS")
fi
if [ -n "$IN_LDFLAGS" ]; then
args+=("LDFLAGS=$IN_LDFLAGS")
fi
./configure ${args[@]+"${args[@]}"}
146 changes: 66 additions & 80 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,82 +24,73 @@ jobs:
env:
CC: ${{ matrix.compiler }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
mysql-server libmysqlclient-dev \
libsnmp-dev libssl-dev build-essential \
help2man autoconf automake libtool dos2unix libcmocka-dev

- name: Prepare for Spine Build
run: |
./bootstrap
./configure --enable-warnings
- uses: ./.github/actions/build-spine
with:
configure-args: --enable-warnings

- name: Build Spine
run: |
set -euo pipefail
make -j"$(nproc)"

- name: Run the unit tests
run: |
set -euo pipefail
make check

# cppcheck:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
#
# - name: Install cppcheck
# run: |
# sudo apt-get update
# sudo apt-get install -y cppcheck build-essential
#
# - name: Run cppcheck
# run: |
# cppcheck \
# --enable=all \
# --std=c11 \
# --error-exitcode=1 \
# --suppress=missingIncludeSystem \
# --suppress=unusedFunction \
# --suppress=checkersReport \
# --suppress=variableScope \
# --suppress=unreadVariable \
# --suppress=shadowVariable \
# --suppress=constVariablePointer \
# --suppress=redundantAssignment \
# --suppress=toomanyconfigs \
# *.c *.h
# test_ping_icmp needs a raw ICMP socket, which the runner user cannot
# open, so under `make check` above it skips and reports nothing. Running
# it with privilege is the only way those cases actually execute, and
# ping_icmp() is a SUID-root path, so a silent skip is the wrong default.
- name: Run the privileged ping tests
run: |
set -euo pipefail
sudo ./tests/unit/test_ping_icmp
echo "raw ICMP cases executed rather than skipped"

net-snmp-no-md5:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Simulate an MD5-disabled/FIPS net-snmp header. The explicit SHA-1
# fallback must configure and link without SNMP_DEFAULT_AUTH_PROTO.
- uses: ./.github/actions/build-spine
with:
configure-args: CPPFLAGS=-DNETSNMP_DISABLE_MD5

- name: Build without net-snmp MD5 auth
run: |
set -euo pipefail
make -j"$(nproc)"
make check

# Coverage is reported, not gated. The number is only meaningful once
# something measures it; the threshold conversation comes after that.
coverage:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y mysql-server libmysqlclient-dev libsnmp-dev \
libssl-dev build-essential help2man autoconf automake libtool \
dos2unix libcmocka-dev gcovr
- uses: ./.github/actions/build-spine
with:
extra-packages: gcovr
cflags: -g -O0 --coverage
ldflags: --coverage

- name: Build instrumented
run: |
set -euo pipefail
./bootstrap
./configure CFLAGS="-g -O0 --coverage" LDFLAGS="--coverage"
make -j"$(nproc)"

- name: Run the unit tests
run: make check
run: |
set -euo pipefail
make check

- name: Report coverage
run: |
Expand All @@ -113,9 +104,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.x"

Expand All @@ -127,71 +118,66 @@ jobs:
# informational so we have a baseline to chip away at.
- name: Run flawfinder
run: |
set -euo pipefail
flawfinder \
--minlevel=3 \
--error-level=5 \
--columns \
--context \
. | tee flawfinder-report.txt

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: flawfinder-report
path: flawfinder-report.txt

# Spine is a threaded network daemon; ASan and UBSan are the checks most
# likely to catch a real defect here.
# likely to catch a real defect here. The unit tests are what actually
# exercises the code, so they are what runs instrumented.
sanitizers:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CC: clang
ASAN_OPTIONS: detect_leaks=1:exitcode=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y clang mysql-server libmysqlclient-dev libsnmp-dev \
libssl-dev build-essential help2man autoconf automake libtool dos2unix libcmocka-dev
- uses: ./.github/actions/build-spine
with:
extra-packages: clang
cflags: -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1
ldflags: -fsanitize=address,undefined

- name: Build with ASan and UBSan
env:
CC: clang
run: |
set -euo pipefail
./bootstrap
./configure \
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1" \
LDFLAGS="-fsanitize=address,undefined"
make -j"$(nproc)"

- name: Run the binary under the sanitizers
env:
ASAN_OPTIONS: detect_leaks=1:exitcode=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
- name: Run the unit tests under the sanitizers
run: |
set -euo pipefail
make check

- name: Smoke-test the binary under the sanitizers
run: |
set -euo pipefail
./spine --version
./spine --help > /dev/null

# A release tarball that cannot be compiled is a release blocker, and the
# only way to catch it is to build from the tarball the way a packager does.
distcheck:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y mysql-server libmysqlclient-dev libsnmp-dev \
libssl-dev build-essential help2man autoconf automake libtool dos2unix libcmocka-dev
- uses: ./.github/actions/build-spine

- name: make distcheck
run: |
set -euo pipefail
./bootstrap
./configure
make distcheck
19 changes: 5 additions & 14 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y clang libmysqlclient-dev libsnmp-dev libssl-dev \
autoconf automake libtool help2man pkg-config

- name: Generate config.h
run: |
set -euo pipefail
./bootstrap
./configure
- uses: ./.github/actions/build-spine
with:
extra-packages: clang pkg-config

# A pull request gets a short regression run against the committed corpus.
# The weekly schedule runs long enough to explore.
Expand All @@ -47,7 +38,7 @@ jobs:
fi
make -C tests/fuzz FUZZ_SECONDS="$seconds" run

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: fuzz-crashes
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Brings up MariaDB and an snmpd, then polls a real device through
# poll_host() and checks the value lands in poller_output.
- name: Poll an SNMPv3 device end to end
run: tests/snmpv3/scripts/run-integration.sh
run: |
set -euo pipefail
tests/snmpv3/scripts/run-integration.sh

- name: Show container logs on failure
if: failure()
run: |
set -euo pipefail
cd tests/snmpv3
docker compose logs --no-color || true
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,21 @@ libtool
*.o
*.m4
tests/unit/bin/

# automake test-suite artifacts
*.trs
test-suite.log

# coverage instrumentation from --enable-coverage
*.gcda
*.gcno
*.gcov

# make dist output
spine-poller-*.tar.gz

# cmocka test binaries and automake stamps
tests/unit/test_*
!tests/unit/test_*.c
.dirstamp
tests/unit/.dirstamp
19 changes: 19 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ The Cacti Group | spine
-issue#552: Terminate die() output so consecutive fatal messages no longer run together
-issue#561: Reserve room for the terminator in php_readpipe() so a full script server result cannot write past result_string
-issue#562: Escalate PHP script server shutdown to SIGKILL after a bounded grace period so a stuck child is not orphaned
-issue#564: Check the calloc() results in spine.c so an allocation failure dies instead of dereferencing NULL
-issue#565: Bound the newline appended by spine_log() so a full log line cannot write past flogmessage
-issue#566: Free the result set on the NULL-row branch of the util.c settings helpers
-issue#567: Build the log timestamp format once at config load instead of rebuilding it on every log line
-issue#600: Wipe the local SNMPv3 passphrase copies in snmp_host_init() before freeing them
-issue#605: Refuse a script server descriptor at or past FD_SETSIZE instead of writing outside fd_set
-issue#606: Allocate the undefined-value string in php_cmd() only once the retry budget is spent
-issue#607: Catch SIGPIPE rather than treating it as fatal, while preserving default SIGPIPE behavior in executed scripts
-issue: Compile output_regex patterns as POSIX extended regular expressions so REGEX_NUMBER works as intended, and apply configured patterns consistently to SNMP, script, and PHP script-server results; script and PHP script-server items previously ignored this field, and a match against multipart output now selects one substring rather than preserving every name:value pair
-upgrade: output_regex now uses POSIX ERE syntax; escape literal parentheses and plus signs, and review patterns written for the former BRE behavior. Spine's built-in numeric fallback will also begin extracting the first decimal from responses it previously rejected as U (for example, 10.0.0.1 becomes 10.0, an OID such as 1.3.6.1 becomes 1.3, and a version such as 2.6.32 becomes 2.6), so review affected data sources before upgrading
-issue: Give scripts the configured timeout to exit naturally, then escalate through SIGTERM and SIGKILL during nft_pclose
-issue: Clear the destination in db_escape() when the column is NULL
-issue: Zero the details array so an early poller exit cannot free uninitialised pointers
-issue: Terminate error_string so the host_errors insert cannot read uninitialised heap
-issue: Match last_snmp_community to the width of the field it is compared against
-issue: Require MySQL 8.0.20 before emitting the row alias upsert form
-issue#608: Bound the script server restart so a server that starts but never answers cannot recurse
-issue#610: Detach the subprocess entry under ListMutex so nft_pclose() owns the child it reaps
-issue#611: Leave hostinfo untouched when getaddrinfo() fails in init_sockaddr()
-issue: Correct signed and unsigned printf format specifiers in poller.c, free session.localname on the unknown-version return in snmp.c, and quote shell variables in the build scripts
-issue: Escape the SNMP result and RRD name before the poller_output INSERT, bound the buffer_output_errors write to the space left in error_string, and validate the --hostlist argument before it reaches SQL
-issue: Restore the twelve headers and spine.conf.dist missing from the dist tarball so a release tarball can be compiled from
Expand Down
Loading