fix(runtime): [develop] retain reviewed probe and compiler stages after result normalization (#660)

The runtime/probe changes from #642 and GNU C17/build changes from #643
were squash-merged into #641's source branch, which made the
result-normalization review include later stages. This follow-up
preserves those approved changes while restoring #641's focused diff
against develop. No force-push or approved-code deletion is required.

Merge after #641 and before #644. #644 will target this branch. This
combines the already reviewed steps 5 and 6; it introduces no new
worker-stack or module-extraction changes.

The UDP follow-up also corrects a validated finding: EHOSTUNREACH on a
connected socket is ambiguous and can represent an unreachable host or
failed neighbour lookup. It now reports down; ECONNREFUSED/ECONNRESET
and actual peer datagrams retain their existing alive behavior. The
production-linked test rejects the original EHOSTUNREACH behavior.

Validation: Ubuntu 24.04/aarch64, clean GCC build and make check: 17
passed, one existing setuid prerequisite skip, zero failures. The full
tracked source tree is byte-identical to the independently validated
#641 functional-fix snapshot. The UDP and poll-host regressions fail
against the original code and pass after the fixes. The isolated step-4
tree independently passes 15 tests with one existing skip. No live
endpoints, host installation, privilege changes, or test suppression.

Repository squash merges still require restacking/retargeting the next
child after each merge; clean ordinary merge simulations are
insufficient by themselves.

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-10-19 14:17:35 -04:00
1 parent 8e741b36cd
commit 9582b71704
15 files changed
+1622 -113

No files matched your search

+15 -2
View File
@@ -69,7 +69,7 @@ test_spine_faults_LDFLAGS = $(AM_LDFLAGS) -Wl,--wrap=strftime -Wl,--wrap=mysql_q
-Wl,--wrap=mysql_num_rows -Wl,--wrap=mysql_fetch_row -Wl,--wrap=mysql_free_result -Wl,--wrap=mysql_num_fields \
-Wl,--wrap=pipe -Wl,--wrap=socketpair -Wl,--wrap=posix_spawn -Wl,--wrap=posix_spawnattr_setpgroup \
-Wl,--wrap=malloc -Wl,--wrap=strdup \
-Wl,--wrap=snmp_sess_open -Wl,--wrap=snmp_sess_close -Wl,--wrap=pthread_create \
-Wl,--wrap=snmp_sess_open -Wl,--wrap=snmp_sess_close -Wl,--wrap=pthread_create -Wl,--wrap=pthread_attr_setstacksize \
-Wl,--wrap=getuid -Wl,--wrap=geteuid -Wl,--wrap=getgid -Wl,--wrap=getegid \
-Wl,--wrap=setuid -Wl,--wrap=setgid -Wl,--wrap=seteuid -Wl,--wrap=setegid -Wl,--wrap=socket
endif
@@ -125,7 +125,9 @@ check_PROGRAMS += \
tests/unit/test_classify_result \
tests/unit/test_util_paths \
tests/unit/test_snmpv3_session \
tests/unit/test_ping_icmp
tests/unit/test_ping_icmp \
tests/unit/test_ping_timing \
tests/unit/test_log_config
# A small executable which speaks the subset of the PHP Script Server protocol
# needed by test_php_runtime. Keeping it as a real child covers the shipped
@@ -205,6 +207,17 @@ tests_unit_test_poll_host_LDFLAGS = \
-Wl,--wrap=snmp_get -Wl,--wrap=snmp_get_allow_fail -Wl,--wrap=snmp_get_multi \
-Wl,--wrap=ping_host
# Includes ping.c directly so its socket calls can be steered; see the file.
tests_unit_test_ping_timing_SOURCES = tests/unit/test_ping_timing.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c \
src/core/keywords.c error.c
tests_unit_test_ping_timing_LDADD = $(CMOCKA_LIBS) $(LIBS)
tests_unit_test_log_config_SOURCES = tests/unit/test_log_config.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c ping.c \
src/core/keywords.c error.c
tests_unit_test_log_config_LDADD = $(CMOCKA_LIBS) $(LIBS)
tests_unit_test_php_runtime_SOURCES = tests/unit/test_php_runtime.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c ping.c \
src/core/keywords.c error.c
+77 -3
View File
@@ -123,7 +123,9 @@ check_PROGRAMS = test_spine_regressions$(EXEEXT) $(am__EXEEXT_1) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_classify_result \
@HAVE_CMOCKA_TRUE@ tests/unit/test_util_paths \
@HAVE_CMOCKA_TRUE@ tests/unit/test_snmpv3_session \
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp \
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_timing \
@HAVE_CMOCKA_TRUE@ tests/unit/test_log_config
@HAVE_CMOCKA_TRUE@noinst_PROGRAMS = \
@HAVE_CMOCKA_TRUE@ tests/unit/php_test_server$(EXEEXT)
@@ -157,7 +159,9 @@ am__installdirs = "$(DESTDIR)$(bindir)" "$(DESTDIR)$(man1dir)" \
@HAVE_CMOCKA_TRUE@ tests/unit/test_classify_result$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_util_paths$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_snmpv3_session$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp$(EXEEXT)
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_timing$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_log_config$(EXEEXT)
@HAVE_CMOCKA_TRUE@@HAVE_LD_WRAP_TRUE@am__EXEEXT_3 = tests/unit/test_php_runtime$(EXEEXT) \
@HAVE_CMOCKA_TRUE@@HAVE_LD_WRAP_TRUE@ tests/unit/test_poll_host$(EXEEXT)
PROGRAMS = $(bin_PROGRAMS) $(noinst_PROGRAMS)
@@ -263,6 +267,16 @@ tests_unit_test_linked_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
$(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
$(AM_CFLAGS) $(CFLAGS) $(tests_unit_test_linked_LDFLAGS) \
$(LDFLAGS) -o $@
am_tests_unit_test_log_config_OBJECTS = \
tests/unit/test_log_config.$(OBJEXT) \
tests/fuzz/stubs.$(OBJEXT) sql.$(OBJEXT) util.$(OBJEXT) \
snmp.$(OBJEXT) src/core/locks.$(OBJEXT) poller.$(OBJEXT) \
nft_popen.$(OBJEXT) php.$(OBJEXT) ping.$(OBJEXT) \
src/core/keywords.$(OBJEXT) error.$(OBJEXT)
tests_unit_test_log_config_OBJECTS = \
$(am_tests_unit_test_log_config_OBJECTS)
tests_unit_test_log_config_DEPENDENCIES = $(am__DEPENDENCIES_1) \
$(am__DEPENDENCIES_1)
am_tests_unit_test_php_runtime_OBJECTS = \
tests/unit/test_php_runtime-test_php_runtime.$(OBJEXT) \
tests/fuzz/unit_test_php_runtime-stubs.$(OBJEXT) \
@@ -291,6 +305,16 @@ tests_unit_test_ping_icmp_OBJECTS = \
$(am_tests_unit_test_ping_icmp_OBJECTS)
tests_unit_test_ping_icmp_DEPENDENCIES = $(am__DEPENDENCIES_1) \
$(am__DEPENDENCIES_1)
am_tests_unit_test_ping_timing_OBJECTS = \
tests/unit/test_ping_timing.$(OBJEXT) \
tests/fuzz/stubs.$(OBJEXT) sql.$(OBJEXT) util.$(OBJEXT) \
snmp.$(OBJEXT) src/core/locks.$(OBJEXT) poller.$(OBJEXT) \
nft_popen.$(OBJEXT) php.$(OBJEXT) src/core/keywords.$(OBJEXT) \
error.$(OBJEXT)
tests_unit_test_ping_timing_OBJECTS = \
$(am_tests_unit_test_ping_timing_OBJECTS)
tests_unit_test_ping_timing_DEPENDENCIES = $(am__DEPENDENCIES_1) \
$(am__DEPENDENCIES_1)
am_tests_unit_test_poll_host_OBJECTS = \
tests/unit/test_poll_host.$(OBJEXT) tests/fuzz/stubs.$(OBJEXT) \
sql.$(OBJEXT) util.$(OBJEXT) snmp.$(OBJEXT) \
@@ -410,8 +434,10 @@ am__depfiles_remade = ./$(DEPDIR)/error.Po ./$(DEPDIR)/nft_popen.Po \
tests/unit/$(DEPDIR)/test_build_fixes.Po \
tests/unit/$(DEPDIR)/test_classify_result.Po \
tests/unit/$(DEPDIR)/test_linked-test_linked.Po \
tests/unit/$(DEPDIR)/test_log_config.Po \
tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po \
tests/unit/$(DEPDIR)/test_ping_icmp.Po \
tests/unit/$(DEPDIR)/test_ping_timing.Po \
tests/unit/$(DEPDIR)/test_poll_host.Po \
tests/unit/$(DEPDIR)/test_safety_fixes.Po \
tests/unit/$(DEPDIR)/test_snmpv3_session.Po \
@@ -442,8 +468,10 @@ SOURCES = $(spine_SOURCES) $(test_spine_faults_SOURCES) \
$(tests_unit_test_build_fixes_SOURCES) \
$(tests_unit_test_classify_result_SOURCES) \
$(tests_unit_test_linked_SOURCES) \
$(tests_unit_test_log_config_SOURCES) \
$(tests_unit_test_php_runtime_SOURCES) \
$(tests_unit_test_ping_icmp_SOURCES) \
$(tests_unit_test_ping_timing_SOURCES) \
$(tests_unit_test_poll_host_SOURCES) \
$(tests_unit_test_safety_fixes_SOURCES) \
$(tests_unit_test_snmpv3_session_SOURCES) \
@@ -455,8 +483,10 @@ DIST_SOURCES = $(spine_SOURCES) $(am__test_spine_faults_SOURCES_DIST) \
$(tests_unit_test_build_fixes_SOURCES) \
$(tests_unit_test_classify_result_SOURCES) \
$(tests_unit_test_linked_SOURCES) \
$(tests_unit_test_log_config_SOURCES) \
$(tests_unit_test_php_runtime_SOURCES) \
$(tests_unit_test_ping_icmp_SOURCES) \
$(tests_unit_test_ping_timing_SOURCES) \
$(tests_unit_test_poll_host_SOURCES) \
$(tests_unit_test_safety_fixes_SOURCES) \
$(tests_unit_test_snmpv3_session_SOURCES) \
@@ -873,7 +903,7 @@ test_spine_regressions_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir) -DSPINE_TEST_PROGRA
@HAVE_LINKER_WRAP_TRUE@ -Wl,--wrap=mysql_num_rows -Wl,--wrap=mysql_fetch_row -Wl,--wrap=mysql_free_result -Wl,--wrap=mysql_num_fields \
@HAVE_LINKER_WRAP_TRUE@ -Wl,--wrap=pipe -Wl,--wrap=socketpair -Wl,--wrap=posix_spawn -Wl,--wrap=posix_spawnattr_setpgroup \
@HAVE_LINKER_WRAP_TRUE@ -Wl,--wrap=malloc -Wl,--wrap=strdup \
@HAVE_LINKER_WRAP_TRUE@ -Wl,--wrap=snmp_sess_open -Wl,--wrap=snmp_sess_close -Wl,--wrap=pthread_create \
@HAVE_LINKER_WRAP_TRUE@ -Wl,--wrap=snmp_sess_open -Wl,--wrap=snmp_sess_close -Wl,--wrap=pthread_create -Wl,--wrap=pthread_attr_setstacksize \
@HAVE_LINKER_WRAP_TRUE@ -Wl,--wrap=getuid -Wl,--wrap=geteuid -Wl,--wrap=getgid -Wl,--wrap=getegid \
@HAVE_LINKER_WRAP_TRUE@ -Wl,--wrap=setuid -Wl,--wrap=setgid -Wl,--wrap=seteuid -Wl,--wrap=setegid -Wl,--wrap=socket
@@ -963,6 +993,18 @@ tests_unit_test_poll_host_LDFLAGS = \
-Wl,--wrap=snmp_get -Wl,--wrap=snmp_get_allow_fail -Wl,--wrap=snmp_get_multi \
-Wl,--wrap=ping_host
# Includes ping.c directly so its socket calls can be steered; see the file.
tests_unit_test_ping_timing_SOURCES = tests/unit/test_ping_timing.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c \
src/core/keywords.c error.c
tests_unit_test_ping_timing_LDADD = $(CMOCKA_LIBS) $(LIBS)
tests_unit_test_log_config_SOURCES = tests/unit/test_log_config.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c ping.c \
src/core/keywords.c error.c
tests_unit_test_log_config_LDADD = $(CMOCKA_LIBS) $(LIBS)
tests_unit_test_php_runtime_SOURCES = tests/unit/test_php_runtime.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c ping.c \
src/core/keywords.c error.c
@@ -1202,6 +1244,12 @@ src/core/tests_unit_test_linked-keywords.$(OBJEXT): \
tests/unit/test_linked$(EXEEXT): $(tests_unit_test_linked_OBJECTS) $(tests_unit_test_linked_DEPENDENCIES) $(EXTRA_tests_unit_test_linked_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_linked$(EXEEXT)
$(AM_V_CCLD)$(tests_unit_test_linked_LINK) $(tests_unit_test_linked_OBJECTS) $(tests_unit_test_linked_LDADD) $(LIBS)
tests/unit/test_log_config.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp)
tests/unit/test_log_config$(EXEEXT): $(tests_unit_test_log_config_OBJECTS) $(tests_unit_test_log_config_DEPENDENCIES) $(EXTRA_tests_unit_test_log_config_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_log_config$(EXEEXT)
$(AM_V_CCLD)$(LINK) $(tests_unit_test_log_config_OBJECTS) $(tests_unit_test_log_config_LDADD) $(LIBS)
tests/unit/test_php_runtime-test_php_runtime.$(OBJEXT): \
tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp)
@@ -1222,6 +1270,12 @@ tests/unit/test_ping_icmp.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/test_ping_icmp$(EXEEXT): $(tests_unit_test_ping_icmp_OBJECTS) $(tests_unit_test_ping_icmp_DEPENDENCIES) $(EXTRA_tests_unit_test_ping_icmp_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_ping_icmp$(EXEEXT)
$(AM_V_CCLD)$(LINK) $(tests_unit_test_ping_icmp_OBJECTS) $(tests_unit_test_ping_icmp_LDADD) $(LIBS)
tests/unit/test_ping_timing.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp)
tests/unit/test_ping_timing$(EXEEXT): $(tests_unit_test_ping_timing_OBJECTS) $(tests_unit_test_ping_timing_DEPENDENCIES) $(EXTRA_tests_unit_test_ping_timing_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_ping_timing$(EXEEXT)
$(AM_V_CCLD)$(LINK) $(tests_unit_test_ping_timing_OBJECTS) $(tests_unit_test_ping_timing_LDADD) $(LIBS)
tests/unit/test_poll_host.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp)
@@ -1332,8 +1386,10 @@ distclean-compile:
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_build_fixes.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_classify_result.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_linked-test_linked.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_log_config.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_ping_icmp.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_ping_timing.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_poll_host.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_safety_fixes.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_snmpv3_session.Po@am__quote@ # am--include-marker
@@ -2484,6 +2540,20 @@ tests/unit/test_ping_icmp.log: tests/unit/test_ping_icmp$(EXEEXT)
--log-file $$b.log --trs-file $$b.trs \
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
"$$tst" $(AM_TESTS_FD_REDIRECT)
tests/unit/test_ping_timing.log: tests/unit/test_ping_timing$(EXEEXT)
@p='tests/unit/test_ping_timing$(EXEEXT)'; \
b='tests/unit/test_ping_timing'; \
$(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
--log-file $$b.log --trs-file $$b.trs \
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
"$$tst" $(AM_TESTS_FD_REDIRECT)
tests/unit/test_log_config.log: tests/unit/test_log_config$(EXEEXT)
@p='tests/unit/test_log_config$(EXEEXT)'; \
b='tests/unit/test_log_config'; \
$(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
--log-file $$b.log --trs-file $$b.trs \
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
"$$tst" $(AM_TESTS_FD_REDIRECT)
tests/unit/test_php_runtime.log: tests/unit/test_php_runtime$(EXEEXT)
@p='tests/unit/test_php_runtime$(EXEEXT)'; \
b='tests/unit/test_php_runtime'; \
@@ -2811,8 +2881,10 @@ distclean: distclean-am
-rm -f tests/unit/$(DEPDIR)/test_build_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_classify_result.Po
-rm -f tests/unit/$(DEPDIR)/test_linked-test_linked.Po
-rm -f tests/unit/$(DEPDIR)/test_log_config.Po
-rm -f tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po
-rm -f tests/unit/$(DEPDIR)/test_ping_icmp.Po
-rm -f tests/unit/$(DEPDIR)/test_ping_timing.Po
-rm -f tests/unit/$(DEPDIR)/test_poll_host.Po
-rm -f tests/unit/$(DEPDIR)/test_safety_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_snmpv3_session.Po
@@ -2935,8 +3007,10 @@ maintainer-clean: maintainer-clean-am
-rm -f tests/unit/$(DEPDIR)/test_build_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_classify_result.Po
-rm -f tests/unit/$(DEPDIR)/test_linked-test_linked.Po
-rm -f tests/unit/$(DEPDIR)/test_log_config.Po
-rm -f tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po
-rm -f tests/unit/$(DEPDIR)/test_ping_icmp.Po
-rm -f tests/unit/$(DEPDIR)/test_ping_timing.Po
-rm -f tests/unit/$(DEPDIR)/test_poll_host.Po
-rm -f tests/unit/$(DEPDIR)/test_safety_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_snmpv3_session.Po
+171
View File
@@ -93,7 +93,49 @@ esac
# Checks for programs.
AC_PROG_AWK
spine_user_cc=$CC
AC_PROG_CC
# ****************** C language standard ***********************
dnl Spine is written to C17 with GNU extensions (docs/adr/0001). Name it
dnl explicitly: the compiler default drifts, and GCC 15 moved it to gnu23.
dnl Autoconf 2.70 and later append their own -std to CC, overriding one the
dnl caller put there, so drop it. A -std in the caller's CC or CFLAGS wins.
if test -n "$spine_user_cc"; then
CC=$spine_user_cc
else
CC=`echo "$CC" | sed 's/ -std=[[^ ]]*//g'`
fi
spine_user_std=
for spine_flag in $spine_user_cc $CFLAGS; do
case $spine_flag in
-std=*) spine_user_std=$spine_flag ;;
esac
done
if test -z "$spine_user_std"; then
AC_MSG_CHECKING([whether $CC accepts -std=gnu17])
CFLAGS="$CFLAGS -std=gnu17"
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [])],
[AC_MSG_RESULT([yes])],
[AC_MSG_RESULT([no])
AC_MSG_ERROR([$CC does not accept -std=gnu17. Spine needs a C17 compiler: GCC 8, Clang 6 or newer.])])
fi
AC_MSG_CHECKING([whether the compiler is in C17 mode or later])
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201710L
#error older than C17
#endif
]], [])],
[AC_MSG_RESULT([yes])],
[AC_MSG_RESULT([no])
if test -n "$spine_user_std"; then
AC_MSG_WARN([$spine_user_std selects a standard older than C17; Spine is developed and tested as gnu17])
else
AC_MSG_ERROR([__STDC_VERSION__ is below 201710L under -std=gnu17])
fi])
AC_PROG_CPP
AC_PROG_INSTALL
AC_PROG_LN_S
@@ -538,6 +580,135 @@ size_t __wrap_strftime(char *out, size_t size, const char *format, const struct
esac
LDFLAGS=$spine_saved_ldflags
AM_CONDITIONAL([HAVE_LINKER_WRAP], [test "$spine_linker_wrap" = yes])
# ****************** Compiler and Linker Hardening ***********************
dnl Probe each flag with -Werror: clang only warns about options it ignores
dnl (-fstack-clash-protection and -pie on Darwin), and a warning would let the
dnl flag through without the protection. The accepted flags go in front of
dnl CPPFLAGS, CFLAGS and LDFLAGS so an explicit -fno-..., -no-pie or
dnl -U_FORTIFY_SOURCE from a packager or user still wins.
AC_ARG_ENABLE([hardening],
AS_HELP_STRING([--disable-hardening],[Do not add compiler and linker hardening flags (default: enabled)]),
[enable_hardening=$enableval],
[enable_hardening=yes])
AC_DEFUN([SPINE_HARDEN_CFLAG],
[AS_VAR_PUSHDEF([spine_cv], [spine_cv_harden_cflag_$1])dnl
AC_CACHE_CHECK([whether $CC accepts $1], [spine_cv],
[spine_save_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS $1 -Werror"
AC_COMPILE_IFELSE([AC_LANG_PROGRAM()],
[AS_VAR_SET([spine_cv], [yes])],
[AS_VAR_SET([spine_cv], [no])])
CFLAGS=$spine_save_CFLAGS])
AS_VAR_IF([spine_cv], [yes], [spine_hardening_cflags="$spine_hardening_cflags $1"])
AS_VAR_POPDEF([spine_cv])])
dnl Link probes run without LIBS: on Linux LIBS already names -lssl, and a
dnl missing library must not be mistaken for an unsupported linker flag.
AC_DEFUN([SPINE_HARDEN_LDFLAG],
[AS_VAR_PUSHDEF([spine_cv], [spine_cv_harden_ldflag_$1])dnl
AC_CACHE_CHECK([whether the linker accepts $1], [spine_cv],
[spine_save_CFLAGS=$CFLAGS
spine_save_LDFLAGS=$LDFLAGS
spine_save_LIBS=$LIBS
CFLAGS="$spine_hardening_cflags $CFLAGS -Werror"
LDFLAGS="$spine_hardening_ldflags $1 $LDFLAGS"
LIBS=
AC_LINK_IFELSE([AC_LANG_PROGRAM()],
[AS_VAR_SET([spine_cv], [yes])],
[AS_VAR_SET([spine_cv], [no])])
CFLAGS=$spine_save_CFLAGS
LDFLAGS=$spine_save_LDFLAGS
LIBS=$spine_save_LIBS])
AS_VAR_IF([spine_cv], [yes], [spine_hardening_ldflags="$spine_hardening_ldflags $1"])
AS_VAR_POPDEF([spine_cv])])
spine_hardening_cppflags=
spine_hardening_cflags=
spine_hardening_ldflags=
if test "x$enable_hardening" != xno; then
SPINE_HARDEN_CFLAG([-fstack-protector-strong])
SPINE_HARDEN_CFLAG([-fstack-clash-protection])
SPINE_HARDEN_CFLAG([-fcf-protection])
SPINE_HARDEN_CFLAG([-Wformat -Wformat-security])
dnl -static and -pie produce different, conflicting link modes. A caller
dnl that opts out of PIE keeps a non-PIE build: -fPIE in front of
dnl -fno-pie would compile non-PIE objects that the -pie link then rejects.
spine_pie_optout=no
case " $CFLAGS $LDFLAGS " in
*" -fno-pie "*|*" -fno-PIE "*|*" -no-pie "*|*" -nopie "*) spine_pie_optout=yes ;;
esac
if test "x$with_static" != xyes && test "x$spine_pie_optout" != xyes; then
SPINE_HARDEN_CFLAG([-fPIE])
case " $spine_hardening_cflags " in
*" -fPIE "*)
SPINE_HARDEN_LDFLAG([-pie])
case " $spine_hardening_ldflags " in
*" -pie "*) ;;
*) spine_hardening_cflags=`echo "$spine_hardening_cflags" | sed 's/ -fPIE//'` ;;
esac
;;
esac
fi
SPINE_HARDEN_LDFLAG([-Wl,-z,relro,-z,now])
SPINE_HARDEN_LDFLAG([-Wl,-z,noexecstack])
dnl Leave _FORTIFY_SOURCE alone when the compiler, a distro spec file or
dnl the caller already set it, so a level 3 default is never lowered and
dnl the macro is never redefined. glibc ignores it, with a warning, below
dnl -O1, and it hides overflows from the ASan interceptors.
AC_CACHE_CHECK([whether _FORTIFY_SOURCE is already defined],
[spine_cv_fortify_preset],
[AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
#include <stdio.h>
#ifndef _FORTIFY_SOURCE
#error not defined
#endif
]])],
[spine_cv_fortify_preset=yes],
[spine_cv_fortify_preset=no])])
AC_CACHE_CHECK([whether optimization is enabled],
[spine_cv_optimizing],
[AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
#ifndef __OPTIMIZE__
#error not optimizing
#endif
]])],
[spine_cv_optimizing=yes],
[spine_cv_optimizing=no])])
if test "x$spine_cv_fortify_preset" = xno &&
test "x$spine_cv_optimizing" = xyes &&
test "x$enable_sanitizers" != xyes; then
dnl Level 3 needs __builtin_dynamic_object_size (GCC 12, Clang 9).
AC_CACHE_CHECK([for the highest supported _FORTIFY_SOURCE level],
[spine_cv_fortify_level],
[AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
#if !defined(__has_builtin)
#error no __has_builtin
#elif !__has_builtin(__builtin_dynamic_object_size)
#error no __builtin_dynamic_object_size
#endif
]])],
[spine_cv_fortify_level=3],
[spine_cv_fortify_level=2])])
spine_hardening_cppflags="-D_FORTIFY_SOURCE=$spine_cv_fortify_level"
fi
CPPFLAGS="$spine_hardening_cppflags $CPPFLAGS"
CFLAGS="$spine_hardening_cflags $CFLAGS"
LDFLAGS="$spine_hardening_ldflags $LDFLAGS"
fi
AC_MSG_NOTICE([hardening CPPFLAGS:${spine_hardening_cppflags:+ $spine_hardening_cppflags}])
AC_MSG_NOTICE([hardening CFLAGS:$spine_hardening_cflags])
AC_MSG_NOTICE([hardening LDFLAGS:$spine_hardening_ldflags])
# cmocka is only used by the unit tests under "make check". Absence is not
# fatal; the tests are dropped from check_PROGRAMS instead.
have_cmocka=no
+81
View File
@@ -0,0 +1,81 @@
<!--
SPDX-FileCopyrightText: 2026 The Cacti Group
SPDX-License-Identifier: GPL-2.0-or-later
-->
# 0001: C17 language standard
## Status
Accepted
## Date
2026-10-07
## Context
Spine's guidance said C99, but `configure.ac` never passed `-std`, so each
compiler built Spine in its own default mode. GCC 8 to 14 and current Clang
default to `gnu17`. GCC 15 and later default to `gnu23`, so a distribution
that moves to GCC 15 would start building Spine as C23 with no change in the
tree. Under GCC 16 that already adds `-Wdiscarded-qualifiers` warnings in
`spine.c` that `gnu17` builds do not show.
C17 is the C11 language with defect fixes. It gives Spine `_Static_assert`
for checking struct sizes and buffer limits at compile time, `_Noreturn` for
fatal paths, and `<stdatomic.h>` for counters that today need a mutex.
Spine also depends on POSIX and BSD interfaces such as pthreads,
`getaddrinfo`, `strtok_r`, `clock_gettime` and the `u_char` type in
Net-SNMP's API. It defines no feature-test macros. Strict `-std=c17` makes
glibc hide those declarations, and on Ubuntu 24.04 the build stops in
`snmp.c` at the first `u_char`. Strict mode would need feature-test macros
in every file first.
## Decision
Spine is written to C17 with GNU extensions. `configure` adds `-std=gnu17`
to `CFLAGS` and stops with an error if the compiler rejects it. A second
check confirms that `__STDC_VERSION__` is at least `201710L`. A `-std`
the caller sets in `CC` or `CFLAGS` is kept; if it names an older
standard, `configure` warns and continues.
The supported compiler floor is:
- GCC 8, the system compiler on Rocky Linux 8 and AlmaLinux 8. Those
releases left full vendor support on 2024-05-31 and receive security fixes
only until 2029-05-31, so the floor serves packagers who still build there
and is not a statement of CI coverage;
- Clang 6 and Apple Clang;
- MSVC 2019 16.8 (`/std:c17`), for the planned Windows port.
These rules stay as they are:
- Declare variables at the top of each block in new code. About 90 older
declarations follow statements; leave them until that code changes for
another reason.
- No variable-length arrays.
- Threads use pthreads until libuv's thread API replaces them in Phase 4.
Do not use `<threads.h>`.
- No C23 features until the oldest supported distribution's compiler builds
C23.
## Consequences
The language mode no longer depends on which compiler a packager has. CI
builds with GCC 13 and Clang 18 on Ubuntu 24.04, Apple Clang on macOS, and
GCC 16 on Fedora 44, whose default is `gnu23`. The weekly Rocky Linux
workflow covers GCC 11. No CI job covers the GCC 8 floor yet; it was checked
by hand on Rocky Linux 8.
New code may use `_Static_assert` and `_Noreturn`. Check `<stdatomic.h>`
against the floor before relying on it: C17 makes atomics optional
(`__STDC_NO_ATOMICS__`), and MSVC 2019 does not ship the header.
cppcheck 2.13, the Ubuntu 24.04 package CI installs, accepts `--std=c17` but
still analyses the code as C11. CI keeps `--std=c11` until it uses a newer
cppcheck.
A compiler older than the floor now fails at `configure` time rather than
partway through the build.
+171 -58
View File
@@ -38,6 +38,7 @@
#ifdef SPINE_HAVE_ICMPV6
static int ping_icmp_ipv6(const host_t *host, ping_t *ping);
#endif
static const char *ping_address(const char *hostname, char *address, size_t capacity);
/*! \fn int ping_host(host_t *host, ping_t *ping)
* \brief ping a host to determine if it is reachable for polling
@@ -160,6 +161,25 @@ int ping_snmp(host_t *host, ping_t *ping) {
static int ping_down(ping_t *ping, const char *message);
/*! \fn static void ping_wait_left(double deadline, struct timeval *timeout)
* \brief converts the time left before a monotonic deadline to a timeval
*
* Each ICMP attempt owns a deadline of its own. Measuring from the first
* attempt left every retry with an already spent budget, so one lost packet
* marked the device down, and rint(ms / 1000) plus the millisecond remainder
* stretched a 1500 ms timeout to 2.5 s.
*/
static void ping_wait_left(double deadline, struct timeval *timeout) {
double remaining = deadline - spine_monotonic_time();
if (remaining < 0) {
remaining = 0;
}
timeout->tv_sec = (time_t) remaining;
timeout->tv_usec = (suseconds_t) ((remaining - (double) timeout->tv_sec) * 1000000);
}
#ifdef __CYGWIN__
/*! \fn static void icmp_discard_reply(int icmp_socket, char *buffer)
* \brief consumes a datagram that ping_icmp() has only peeked at
@@ -649,7 +669,7 @@ int ping_icmp(const host_t *host, ping_t *ping) {
icmp_waiter_t waiter;
int waiting = FALSE;
double begin_time, end_time, total_time;
double begin_time, deadline, total_time;
double host_timeout;
double one_thousand = 1000.00;
struct timeval timeout;
@@ -757,7 +777,6 @@ int ping_icmp(const host_t *host, ping_t *ping) {
if (init_sockaddr(&fromname, host->hostname, 7)) {
retry_count = 0;
total_time = 0;
begin_time = get_time_as_double();
if (icmp_use_shared) {
memset(&waiter, 0, sizeof(waiter));
@@ -807,9 +826,9 @@ int ping_icmp(const host_t *host, ping_t *ping) {
continue;
}
/* decrement the timeout value by the total time */
timeout.tv_sec = rint((host_timeout - total_time) / 1000);
timeout.tv_usec = ((int) (host_timeout - total_time) % 1000) * 1000;
begin_time = spine_monotonic_time();
deadline = begin_time + host_timeout / one_thousand;
ping_wait_left(deadline, &timeout);
/* set the socket send and receive timeout */
setsockopt(icmp_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
@@ -832,15 +851,18 @@ int ping_icmp(const host_t *host, ping_t *ping) {
goto cleanup;
}
FD_SET(icmp_socket,&socket_fds);
/* a stray datagram must not restart the wait, and only Linux
* writes the time left back into the timeval */
ping_wait_left(deadline, &timeout);
return_code = select(icmp_socket + 1, &socket_fds, NULL, NULL, &timeout);
/* record end time */
end_time = get_time_as_double();
if (return_code < 0 && errno == EINTR) {
goto keep_listening;
}
/* calculate total time */
total_time = (end_time - begin_time) * one_thousand;
total_time = (spine_monotonic_time() - begin_time) * one_thousand;
if (total_time < host_timeout) {
if (return_code > 0 && total_time < host_timeout) {
#if !(defined(__CYGWIN__))
return_code = recvfrom(icmp_socket, socket_reply, BUFSIZE, MSG_WAITALL, (struct sockaddr *) &recvname, &fromlen);
#else
@@ -904,15 +926,10 @@ int ping_icmp(const host_t *host, ping_t *ping) {
rc = HOST_UP;
goto cleanup;
} else {
/* received a response other than an echo reply */
/* received a response other than an echo reply; the
* echo reply may still arrive inside this deadline */
ICMP_DISCARD_PEEKED(icmp_socket, socket_reply);
if (total_time > host_timeout) {
retry_count++;
total_time = 0;
}
continue;
goto keep_listening;
}
} else {
/* another host responded */
@@ -979,6 +996,7 @@ cleanup:
static int init_sockaddr6(struct sockaddr_in6 *name, const char *hostname) {
struct addrinfo hints, *hostinfo;
int rv, retry_count;
char address[BUFSIZE];
memset(&hints, 0, sizeof(hints));
@@ -992,7 +1010,7 @@ static int init_sockaddr6(struct sockaddr_in6 *name, const char *hostname) {
hostinfo = NULL;
while (TRUE) {
rv = getaddrinfo(hostname, NULL, &hints, &hostinfo);
rv = getaddrinfo(ping_address(hostname, address, sizeof(address)), NULL, &hints, &hostinfo);
if (rv == 0) {
break;
@@ -1092,7 +1110,7 @@ static int ping_icmp_ipv6(const host_t *host, ping_t *ping) {
icmp_waiter_t waiter;
int waiting = FALSE;
double begin_time, end_time, total_time;
double begin_time, deadline, total_time;
double host_timeout;
double one_thousand = 1000.00;
struct timeval timeout;
@@ -1247,7 +1265,6 @@ static int ping_icmp_ipv6(const host_t *host, ping_t *ping) {
retry_count = 0;
total_time = 0;
begin_time = get_time_as_double();
if (icmp_use_shared) {
memset(&waiter, 0, sizeof(waiter));
@@ -1298,9 +1315,9 @@ static int ping_icmp_ipv6(const host_t *host, ping_t *ping) {
continue;
}
/* decrement the timeout value by the total time */
timeout.tv_sec = rint((host_timeout - total_time) / 1000);
timeout.tv_usec = ((int) (host_timeout - total_time) % 1000) * 1000;
begin_time = spine_monotonic_time();
deadline = begin_time + host_timeout / one_thousand;
ping_wait_left(deadline, &timeout);
/* set the socket send and receive timeout */
setsockopt(icmp_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
@@ -1326,13 +1343,16 @@ static int ping_icmp_ipv6(const host_t *host, ping_t *ping) {
goto cleanup;
}
FD_SET(icmp_socket,&socket_fds);
/* a stray datagram must not restart the wait, and only Linux
* writes the time left back into the timeval */
ping_wait_left(deadline, &timeout);
return_code = select(icmp_socket + 1, &socket_fds, NULL, NULL, &timeout);
/* record end time */
end_time = get_time_as_double();
if (return_code < 0 && errno == EINTR) {
goto keep_listening_ipv6;
}
/* calculate total time */
total_time = (end_time - begin_time) * one_thousand;
total_time = (spine_monotonic_time() - begin_time) * one_thousand;
if ((return_code > 0) && (total_time < host_timeout)) {
fromlen = sizeof(recvname);
@@ -1426,16 +1446,23 @@ static int ping_down(ping_t *ping, const char *message) {
return HOST_DOWN;
}
/* UDP reachability is established by the existing ICMP port-error contract;
* receiving an application datagram alone does not establish that result. */
/* A connected socket accepts datagrams only from its peer. Port-unreachable
* errors also indicate a response, but EHOSTUNREACH cannot distinguish a
* firewall rejection from an unreachable host or failed neighbour lookup. It
* is neither proof of life nor a reason to stop retrying, so the wait
* continues until the deadline and the caller decides whether to try again. */
static int ping_udp_response(int fd, double deadline) {
char response[BUFSIZE];
for (;;) {
int ready = spine_wait_readable(fd, deadline);
if (ready <= 0) return ready;
ssize_t received = recv(fd, response, sizeof(response), 0);
if (received >= 0) continue;
if (errno == EHOSTUNREACH || errno == ECONNRESET || errno == ECONNREFUSED) return 1;
if (received >= 0) return 1;
if (errno == ECONNRESET || errno == ECONNREFUSED) return 1;
if (errno == EHOSTUNREACH || errno == ENETUNREACH) continue;
#ifdef EHOSTDOWN
if (errno == EHOSTDOWN) continue;
#endif
if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) return -1;
}
}
@@ -1555,6 +1582,7 @@ int get_address_type(host_t *host) {
struct addrinfo hints;
struct addrinfo *res_list;
char addrstr[255];
char address[BUFSIZE];
const void *ptr = NULL;
int addr_found = FALSE;
@@ -1565,7 +1593,7 @@ int get_address_type(host_t *host) {
hints.ai_flags = AI_CANONNAME | AI_ADDRCONFIG;
int error;
if ((error = getaddrinfo(host->hostname, NULL, &hints, &res_list)) != 0) {
if ((error = getaddrinfo(ping_address(host->hostname, address, sizeof(address)), NULL, &hints, &res_list)) != 0) {
SPINE_LOG(("WARNING: Unable to determine address info for %s (%s)", host->hostname, gai_strerror(error)));
return SPINE_NONE;
}
@@ -1616,10 +1644,12 @@ int init_sockaddr(struct sockaddr_in *name, const char *hostname, int port) {
if (port < 0 || port > 65535) return FALSE;
struct addrinfo hints = {0};
struct addrinfo *hostinfo = NULL;
char address[BUFSIZE];
hints.ai_family = AF_INET;
hints.ai_flags = AI_CANONNAME | AI_ADDRCONFIG;
ping_address(hostname, address, sizeof(address));
for (int attempt = 0; ; attempt++) {
int status = getaddrinfo(hostname, NULL, &hints, &hostinfo);
int status = getaddrinfo(address, NULL, &hints, &hostinfo);
if (status == 0) break;
if (status == EAI_AGAIN && attempt < 3) {
SPINE_LOG(("WARNING: Temporary DNS error for host %s (%s), retrying", hostname, gai_strerror(status)));
@@ -1640,35 +1670,118 @@ int init_sockaddr(struct sockaddr_in *name, const char *hostname, int port) {
return TRUE;
}
/*! \brief Parse a device hostname with an optional transport and port. */
name_t *get_namebyhost(const char *hostname, name_t *name) {
if (name == NULL) {
name = calloc(1, sizeof(*name));
if (name == NULL) die("ERROR: Fatal malloc error: ping.c get_namebyhost->name");
}
/* IPv6 transport addresses are passed intact to the address resolver. */
if (strchr(hostname, '[') != NULL || strstr(hostname, "::") != NULL || char_count(hostname, ':') > 2) {
strncopy(name->hostname, hostname, sizeof(name->hostname));
return name;
}
char *copy = strdup(hostname);
if (copy == NULL) die("ERROR: Fatal malloc error: ping.c get_namebyhost->stack");
char *saveptr = NULL;
const char *token = strtok_r(copy, ":", &saveptr);
/*! \fn static int split_host_spec(const char *spec, int *method, char *address, size_t capacity, int *port)
* \brief splits a "[transport:]host[:port]" device name into its parts
*
* Cacti keeps Net-SNMP transport specifiers such as udp6:[2001:db8::1]:161
* in the hostname. An IPv6 host is either bracketed, so that a port can
* follow it, or a bare literal in which every colon belongs to the address.
* A port that is not a plain number from 1 to 65535 is reported as 0.
*
* \return TRUE if the host part is IPv6, FALSE otherwise
*/
static int split_host_spec(const char *spec, int *method, char *address, size_t capacity, int *port) {
static const char *const methods[] = {"TCP", "UDP", "TCP6", "UDP6"};
if (token != NULL) {
for (size_t i = 0; i < sizeof(methods) / sizeof(methods[0]); i++) {
if (strcasecmp(token, methods[i]) == 0) {
name->method = (int)i + 1;
token = strtok_r(NULL, ":", &saveptr);
const char *host = spec;
const char *colon;
const char *end;
const char *port_text = NULL;
size_t length;
size_t i;
int ipv6 = FALSE;
*method = 0;
*port = 0;
if (capacity == 0) {
return FALSE;
}
colon = strchr(spec, ':');
if (colon != NULL) {
for (i = 0; i < sizeof(methods) / sizeof(methods[0]); i++) {
length = strlen(methods[i]);
if ((size_t) (colon - spec) == length && strncasecmp(spec, methods[i], length) == 0) {
*method = (int) i + 1;
host = colon + 1;
break;
}
}
}
strncopy(name->hostname, token != NULL ? token : hostname, sizeof(name->hostname));
token = strtok_r(NULL, ":", &saveptr);
if (token != NULL) name->port = atoi(token);
free(copy);
if (host[0] == '[' && (end = strchr(host, ']')) != NULL) {
ipv6 = TRUE;
host = host + 1;
length = (size_t) (end - host);
if (end[1] == ':') {
port_text = end + 2;
}
} else if (char_count(host, ':') > 1 || host[0] == '[') {
ipv6 = TRUE;
length = strlen(host);
} else if ((colon = strchr(host, ':')) != NULL) {
length = (size_t) (colon - host);
port_text = colon + 1;
} else {
length = strlen(host);
}
if (length >= capacity) {
length = capacity - 1;
}
snprintf(address, capacity, "%.*s", (int) length, host);
if (port_text != NULL && port_text[0] >= '0' && port_text[0] <= '9') {
char *rest;
long value;
errno = 0;
value = strtol(port_text, &rest, 10);
if (errno == 0 && *rest == '\0' && value > 0 && value <= 65535) {
*port = (int) value;
}
}
return ipv6;
}
/*! \fn static const char *ping_address(const char *hostname, char *address, size_t capacity)
* \brief the bare address a network probe resolves for a device name
*/
static const char *ping_address(const char *hostname, char *address, size_t capacity) {
int method;
int port;
split_host_spec(hostname, &method, address, capacity, &port);
return address;
}
/*! \fn name_t *get_namebyhost(const char *hostname, name_t *name)
* \brief parses a device hostname with an optional transport and port
*
* An IPv6 name is returned whole: Net-SNMP needs the transport and the
* brackets to reach the agent, and the ping code strips them itself.
*/
name_t *get_namebyhost(const char *hostname, name_t *name) {
char address[BUFSIZE];
if (name == NULL) {
name = calloc(1, sizeof(*name));
if (name == NULL) die("ERROR: Fatal malloc error: ping.c get_namebyhost->name");
}
if (split_host_spec(hostname, &name->method, address, sizeof(address), &name->port)) {
strncopy(name->hostname, hostname, sizeof(name->hostname));
} else {
strncopy(name->hostname, address, sizeof(name->hostname));
}
return name;
}
+39 -9
View File
@@ -251,6 +251,20 @@ static void parse_host_list(const char *input) {
}
}
/* atoi() turned "-f abc" into device 0 and let "-t -1" size the connection
* pools, so only a whole unsigned decimal number inside the range passes. */
static bool parse_cli_int(const char *text, long minimum, long maximum, int *value) {
char *end;
long parsed;
if (text[0] < '0' || text[0] > '9') return FALSE;
errno = 0;
parsed = strtol(text, &end, 10);
if (errno != 0 || *end != '\0' || parsed < minimum || parsed > maximum) return FALSE;
*value = (int)parsed;
return TRUE;
}
static void parse_cli_argument(const char *arg, char *opt, char ***argv, char **conf_file) {
switch (lookup_cli_option(arg)) {
case CLI_FIRST: {
@@ -259,9 +273,8 @@ static void parse_cli_argument(const char *arg, char *opt, char ***argv, char **
}
opt = getarg(opt, argv);
set.hosts.start_host_id = atoi(opt);
if (!HOSTID_DEFINED(set.hosts.start_host_id)) {
if (!parse_cli_int(opt, 0, INT_MAX, &set.hosts.start_host_id)) {
die("ERROR: '%s=%s' is invalid first-host ID", arg, opt);
}
break;
@@ -272,9 +285,8 @@ static void parse_cli_argument(const char *arg, char *opt, char ***argv, char **
}
opt = getarg(opt, argv);
set.hosts.end_host_id = atoi(opt);
if (!HOSTID_DEFINED(set.hosts.end_host_id)) {
if (!parse_cli_int(opt, 0, INT_MAX, &set.hosts.end_host_id)) {
die("ERROR: '%s=%s' is invalid last-host ID", arg, opt);
}
break;
@@ -284,7 +296,18 @@ static void parse_cli_argument(const char *arg, char *opt, char ***argv, char **
break;
}
case CLI_THREADS: {
set.poller.threads = atoi(getarg(opt, argv));
opt = getarg(opt, argv);
if (!parse_cli_int(opt, 1, INT_MAX, &set.poller.threads)) {
die("ERROR: '%s=%s' is an invalid thread count, use 1 to %d", arg, opt, MAX_THREADS);
}
/* the database setting is capped the same way, and existing
* invocations above the cap have always run */
if (set.poller.threads > MAX_THREADS) {
fprintf(stderr, "WARNING: '%s=%s' exceeds the maximum thread count, using %d\n", arg, opt, MAX_THREADS);
set.poller.threads = MAX_THREADS;
}
set.poller.threads_set = TRUE;
break;
}
@@ -352,11 +375,15 @@ static void parse_cli_argument(const char *arg, char *opt, char ***argv, char **
}
default:
if (!HOSTID_DEFINED(set.hosts.start_host_id) && all_digits(arg)) {
set.hosts.start_host_id = atoi(arg);
if (!parse_cli_int(arg, 0, INT_MAX, &set.hosts.start_host_id)) {
die("ERROR: '%s' is invalid first-host ID", arg);
}
}
else if (!HOSTID_DEFINED(set.hosts.end_host_id) && all_digits(arg)) {
set.hosts.end_host_id = atoi(arg);
if (!parse_cli_int(arg, 0, INT_MAX, &set.hosts.end_host_id)) {
die("ERROR: '%s' is invalid last-host ID", arg);
}
}
else {
@@ -1183,8 +1210,11 @@ int main(int argc, char *argv[]) {
}
/* initialize threads and mutexes */
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (spine_thread_attr_init(&attr) != 0 ||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0) {
set.exit.exit_code = EXIT_FAILURE;
die("ERROR: Unable to initialize polling thread attributes");
}
init_mutexes();
+10
View File
@@ -97,6 +97,16 @@ void test_cli_alias_contracts(void) {
run_cli_contract(arguments, EXIT_FAILURE, output, sizeof(output));
assert(strstr(output, "ERROR: invalid or oversized host list") != NULL);
}
/* the longest list that fits host_id_list is accepted whole */
char *longest = malloc(BIG_BUFSIZE);
assert(longest != NULL);
for (size_t index = 0; index < BIG_BUFSIZE - 1; index++) longest[index] = index % 2 == 0 ? '7' : ',';
longest[BIG_BUFSIZE - 1] = '\0';
if (longest[BIG_BUFSIZE - 2] == ',') longest[BIG_BUFSIZE - 2] = '\0';
char *longest_args[] = {"spine", "-H", longest, "--version", NULL};
run_cli_contract(longest_args, EXIT_SUCCESS, output, sizeof(output));
assert(strcmp(version, output) == 0);
free(longest);
char *oversized = malloc(BIG_BUFSIZE + 1);
assert(oversized != NULL);
memset(oversized, '0', BIG_BUFSIZE);
+85 -4
View File
@@ -11,6 +11,9 @@
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*/
/* pthread_getattr_np() and pthread_setattr_default_np() check worker stacks. */
#define _GNU_SOURCE
#include "common.h"
#include "spine.h"
#include <dirent.h>
@@ -111,7 +114,10 @@ int __wrap_pthread_create(pthread_t *thread, const pthread_attr_t *attributes,
void *(*start)(void *), void *arg) {
if (!launch_case_active || start != &child) return __real_pthread_create(thread, attributes, start, arg);
const poller_thread_t *work = arg;
size_t stack_size = 0;
assert(work != NULL && work->host_id == 902);
assert(attributes != NULL && pthread_attr_getstacksize(attributes, &stack_size) == 0);
assert(stack_size >= SPINE_THREAD_STACK_SIZE);
launch_calls++;
launch_device_counter = work->device_counter;
launch_startup = work->thread_init_sem;
@@ -321,6 +327,14 @@ int __wrap_socketpair(int domain, int type, int protocol, int descriptors[2]) {
return __real_socketpair(domain, type, protocol, descriptors);
}
extern int __real_pthread_attr_setstacksize(pthread_attr_t *, size_t);
static int stack_size_failures;
int __wrap_pthread_attr_setstacksize(pthread_attr_t *attributes, size_t size) {
if (stack_size_failures > 0) { stack_size_failures--; return EINVAL; }
return __real_pthread_attr_setstacksize(attributes, size);
}
/* Script children are created with posix_spawn(), which reports failure
* through its return value rather than errno. */
int __wrap_posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *actions,
@@ -937,7 +951,64 @@ static void test_fake_database_main_paths(void) {
puts("production scripted database main-thread regressions passed");
}
/* The stack the thread really got, not the one requested. */
static void assert_worker_stack(void) {
pthread_attr_t attributes;
size_t stack_size = 0;
assert(pthread_getattr_np(pthread_self(), &attributes) == 0);
assert(pthread_attr_getstacksize(&attributes, &stack_size) == 0);
assert(pthread_attr_destroy(&attributes) == 0);
assert(stack_size >= SPINE_THREAD_STACK_SIZE);
}
static void start_test_worker(pthread_t *worker, void *(*start)(void *), void *argument) {
pthread_attr_t attributes;
assert(spine_thread_attr_init(&attributes) == 0);
assert(pthread_create(worker, &attributes, start, argument) == 0);
assert(pthread_attr_destroy(&attributes) == 0);
}
/* A larger default is kept, and a refused size is reported, not ignored.
* pthread_setattr_default_np() is a glibc extension; musl lacks it. */
static void test_thread_attr_init(void) {
pthread_attr_t attributes;
size_t stack_size = 0;
stack_size_failures = 1;
assert(spine_thread_attr_init(&attributes) == EINVAL);
assert(stack_size_failures == 0);
#if defined(__GLIBC__)
fflush(NULL);
pid_t process = fork();
assert(process >= 0);
if (process == 0) {
pthread_attr_t larger;
assert(pthread_attr_init(&larger) == 0);
assert(pthread_attr_setstacksize(&larger, 4 * SPINE_THREAD_STACK_SIZE) == 0);
assert(pthread_setattr_default_np(&larger) == 0);
assert(pthread_attr_destroy(&larger) == 0);
assert(spine_thread_attr_init(&attributes) == 0);
assert(pthread_attr_getstacksize(&attributes, &stack_size) == 0);
assert(pthread_attr_destroy(&attributes) == 0);
exit(stack_size == 4 * SPINE_THREAD_STACK_SIZE ? 0 : 1);
}
int status;
assert(waitpid(process, &status, 0) == process);
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
#endif
assert(spine_thread_attr_init(&attributes) == 0);
assert(pthread_attr_getstacksize(&attributes, &stack_size) == 0);
assert(pthread_attr_destroy(&attributes) == 0);
assert(stack_size == SPINE_THREAD_STACK_SIZE);
puts("production worker thread stack regressions passed");
}
static void *fake_poll_worker(void *argument) {
assert_worker_stack();
assert(mysql_thread_init() == 0);
int errors = 0;
poll_host(argument, &errors);
@@ -953,7 +1024,7 @@ static void run_fake_poll(poller_thread_t *device, int host_id) {
STRNCOPY(device->host_time, "1791244800");
set.exit.exit_code = EXIT_SUCCESS;
pthread_t worker;
assert(pthread_create(&worker, NULL, fake_poll_worker, device) == 0);
start_test_worker(&worker, fake_poll_worker, device);
assert(pthread_join(worker, NULL) == 0);
assert(device->threads_complete == 1);
}
@@ -1016,7 +1087,7 @@ static void test_fake_database_poll_host(void) {
device.host_threads = 1;
STRNCOPY(device.host_time, "1791244800");
pthread_t worker;
assert(pthread_create(&worker, NULL, fake_poll_worker, &device) == 0);
start_test_worker(&worker, fake_poll_worker, &device);
assert(pthread_join(worker, NULL) == 0);
assert(device.threads_complete == 0 && !device.poll_failed && db_pool_local[0].free);
/* A row count of one with no row ignores the device but completes it. */
@@ -1263,7 +1334,7 @@ static void test_ping_only_session_lifetime(void) {
session_closes = 0;
account_snmp_sessions = TRUE;
pthread_t worker;
assert(pthread_create(&worker, NULL, run_ping_only_worker, &work) == 0);
start_test_worker(&worker, run_ping_only_worker, &work);
assert(pthread_join(worker, NULL) == 0);
account_snmp_sessions = FALSE;
int observed_opens = session_opens;
@@ -1368,7 +1439,7 @@ static void test_snmp_batch_key_width(void) {
session_closes = 0;
account_snmp_sessions = TRUE;
pthread_t worker;
assert(pthread_create(&worker, NULL, run_batch_worker, &work) == 0);
start_test_worker(&worker, run_batch_worker, &work);
assert(pthread_join(worker, NULL) == 0);
account_snmp_sessions = FALSE;
int observed_opens = session_opens;
@@ -1496,6 +1567,15 @@ static void test_worker_launch_failures(void) {
}
int main(int argc, char **argv) {
#if defined(__GLIBC__)
/* Give unconfigured threads musl's 128 KiB, so a worker started without
* spine_thread_attr_init() overflows here as it does on Alpine. */
pthread_attr_t musl_default;
assert(pthread_attr_init(&musl_default) == 0);
assert(pthread_attr_setstacksize(&musl_default, 128 * 1024) == 0);
assert(pthread_setattr_default_np(&musl_default) == 0);
assert(pthread_attr_destroy(&musl_default) == 0);
#endif
/* Fresh exec enters production initialization exactly once, without
* reinitializing the ordinary fault harness's inherited mutexes. */
if (argc == 4 && strcmp(argv[1], "--worker-launch-case") == 0) {
@@ -1515,6 +1595,7 @@ int main(int argc, char **argv) {
test_privilege_drop_faults();
test_fake_database_wrappers();
test_fake_database_main_paths();
test_thread_attr_init();
test_fake_database_poll_host();
test_fake_database_startup_reads();
test_fake_database_main();
+3 -2
View File
@@ -17,6 +17,7 @@
extern poller_thread_t **details;
extern int *debug_devices;
extern void start_test_worker(pthread_t *worker, void *(*start)(void *), void *argument);
static unsigned long long output_count(MYSQL *mysql, const char *query) {
MYSQL_RES *result = db_query(mysql, LOCAL, query);
@@ -51,7 +52,7 @@ static void run_output_partition(const poller_thread_t *aggregate, int partition
pthread_t worker;
/* Production child owns/frees instructions and releases thread/startup
* permits after poll_host, including output failures. */
assert(pthread_create(&worker, NULL, child, work) == 0);
start_test_worker(&worker, child, work);
assert(pthread_join(worker, NULL) == 0);
assert(spine_permits_available(&startup) == 1);
assert(spine_permits_destroy(&startup) == 0);
@@ -290,7 +291,7 @@ static void test_concurrent_output_failure(MYSQL *mysql) {
workers[index].finished_fd = finished[index][1];
assert(spine_permits_try_acquire(&startup) == 0);
assert(spine_permits_try_acquire(&available_threads) == 0);
assert(pthread_create(&threads[index], NULL, controlled_output_child, &workers[index]) == 0);
start_test_worker(&threads[index], controlled_output_child, &workers[index]);
}
double deadline = spine_monotonic_time() + 10;
await_output_byte(ready[0], 'R', deadline);
+41 -11
View File
@@ -33,6 +33,15 @@ extern void test_reindex_result_contracts(MYSQL *mysql);
extern void test_additional_snmp_session_boundaries(void);
extern void test_snmpv3_agent_contracts(void);
/* Threads that run poll_host() need the stack production workers get. */
void start_test_worker(pthread_t *worker, void *(*start)(void *), void *argument) {
pthread_attr_t attributes;
assert(spine_thread_attr_init(&attributes) == 0);
assert(pthread_create(worker, &attributes, start, argument) == 0);
assert(pthread_attr_destroy(&attributes) == 0);
}
static void test_copy_bounds(void) {
struct { char text[8]; unsigned char guard; } output;
memset(&output, 0xa5, sizeof output);
@@ -125,7 +134,7 @@ static void test_cli_case(const char *flag, const char *input, int expected, con
int status;
assert(waitpid(child, &status, 0) == child);
assert(WIFEXITED(status) && WEXITSTATUS(status) == expected);
if (expected == EXIT_FAILURE) assert(strstr(message, error) != NULL);
if (expected == EXIT_FAILURE || error != NULL) assert(strstr(message, error) != NULL);
else assert(message[0] == '\0');
}
@@ -138,6 +147,27 @@ static void test_cli_option_shape(void) {
test_cli_case("--mode", "offline", EXIT_SUCCESS, NULL);
test_cli_case("--mode", "recovery", EXIT_SUCCESS, NULL);
test_cli_case("--mode", "invalid", EXIT_FAILURE, "ERROR: invalid polling mode 'invalid' specified");
static const char *const bad_threads[] = {"0", "-1", "abc", "5x", " 5", "+5", "", "99999999999999999999"};
for (size_t index = 0; index < sizeof(bad_threads) / sizeof(bad_threads[0]); index++) {
test_cli_case("--threads", bad_threads[index], EXIT_FAILURE, "is an invalid thread count, use 1 to 100");
test_cli_case("-t", bad_threads[index], EXIT_FAILURE, "is an invalid thread count, use 1 to 100");
}
test_cli_case("--threads", "1", EXIT_SUCCESS, NULL);
test_cli_case("-t", "100", EXIT_SUCCESS, NULL);
/* above the cap still runs, as it did with atoi(), but says so */
test_cli_case("-t", "101", EXIT_SUCCESS, "WARNING: '-t=101' exceeds the maximum thread count, using 100");
test_cli_case("--threads", "150", EXIT_SUCCESS, "WARNING: '--threads=150' exceeds the maximum thread count, using 100");
static const char *const bad_ids[] = {"abc", "1x", "-1", "+1", " 1", "", "2147483648", "99999999999999999999"};
for (size_t index = 0; index < sizeof(bad_ids) / sizeof(bad_ids[0]); index++) {
test_cli_case("--first", bad_ids[index], EXIT_FAILURE, "is invalid first-host ID");
test_cli_case("-l", bad_ids[index], EXIT_FAILURE, "is invalid last-host ID");
}
test_cli_case("-f", "0", EXIT_SUCCESS, NULL);
test_cli_case("--last", "2147483647", EXIT_SUCCESS, NULL);
/* the positional "first last" form poller.php uses */
test_cli_case("7", "99999999999999999999", EXIT_FAILURE, "is invalid last-host ID");
test_cli_case("99999999999999999999", "7", EXIT_FAILURE, "is invalid first-host ID");
test_cli_case("7", "9", EXIT_SUCCESS, NULL);
int errors[2];
assert(pipe(errors) == 0);
pid_t child = fork();
@@ -1852,7 +1882,7 @@ static void test_reindex_pipeline(MYSQL *mysql, test_poll_work_t *work) {
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, work) == 0);
start_test_worker(&worker, test_poll_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1);
int expected_errors = 1;
@@ -1886,7 +1916,7 @@ static void test_reindex_queue_failure(MYSQL *mysql, test_poll_work_t *work) {
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, work) == 0);
start_test_worker(&worker, test_poll_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(db_insert(mysql, LOCAL, "RENAME TABLE poller_command_regression_hidden TO poller_command"));
long long kept = (long long)database_count(mysql, "SELECT COUNT(*) FROM poller_reindex WHERE (data_query_id=7 AND assert_value='122') OR (data_query_id=8 AND assert_value='122')");
@@ -1906,7 +1936,7 @@ static void test_reindex_query_shortcut(MYSQL *mysql, test_poll_work_t *work) {
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, work) == 0);
start_test_worker(&worker, test_poll_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 1);
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_command") == 2);
@@ -1935,14 +1965,14 @@ static void test_profile_schedule_completion(MYSQL *mysql, test_poll_work_t *agg
test_poll_work_t second = *aggregate;
second.thread.host_thread = 2;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, &second) == 0);
start_test_worker(&worker, test_poll_worker, &second);
assert(pthread_join(worker, NULL) == 0);
assert(aggregate->thread.threads_complete == 1 && !aggregate->thread.complete);
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=43 AND rrd_next_step=0") == 2);
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=602 AND output='U'") == 1);
test_poll_work_t first = *aggregate;
first.thread.host_thread = 1;
assert(pthread_create(&worker, NULL, test_poll_worker, &first) == 0);
start_test_worker(&worker, test_poll_worker, &first);
assert(pthread_join(worker, NULL) == 0);
assert(aggregate->thread.threads_complete == 2 && aggregate->thread.complete);
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=601 AND output='123') OR (local_data_id=602 AND output='U')") == 2);
@@ -1979,7 +2009,7 @@ static void test_poll_database_failure(MYSQL *mysql, test_poll_work_t *aggregate
aggregate->thread.poll_failed = FALSE;
set.exit.exit_code = EXIT_SUCCESS;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, aggregate) == 0);
start_test_worker(&worker, test_poll_worker, aggregate);
assert(pthread_join(worker, NULL) == 0);
assert(aggregate->thread.threads_complete == 1);
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
@@ -2048,7 +2078,7 @@ static void test_snmp_item_pipeline(MYSQL *mysql, test_poll_work_t *work, const
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, work) == 0);
start_test_worker(&worker, test_poll_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1);
assert(work->errors == (spike ? 0 : 1));
@@ -2090,7 +2120,7 @@ static void test_snmp_flush_contracts(MYSQL *mysql, test_poll_work_t *work, cons
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, work) == 0);
start_test_worker(&worker, test_poll_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1);
const char *expected = spike ? "output='U'" : "output REGEXP '^[0-9]$'";
@@ -2114,7 +2144,7 @@ static void test_snmp_flush_contracts(MYSQL *mysql, test_poll_work_t *work, cons
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t regex_worker;
assert(pthread_create(&regex_worker, NULL, test_poll_worker, work) == 0);
start_test_worker(&regex_worker, test_poll_worker, work);
assert(pthread_join(regex_worker, NULL) == 0);
if (database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id IN (601,602) AND output='266'") != 2) {
fprintf(stderr, "flush contract: output_regex did not see the normalized Hex-STRING\n");
@@ -2198,7 +2228,7 @@ static void test_poll_pipeline(MYSQL *mysql) {
owned_details[0] = &work->thread;
details = owned_details;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, work) == 0);
start_test_worker(&worker, test_poll_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 1);
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
+5 -4
View File
@@ -18,6 +18,7 @@
#endif
extern poller_thread_t **details;
extern void start_test_worker(pthread_t *worker, void *(*start)(void *), void *argument);
typedef struct {
poller_thread_t thread;
@@ -64,7 +65,7 @@ static void run_reindex_case(MYSQL *mysql, int action, const char *argument,
poller_thread_t **prior_details = details;
details = owned_details;
pthread_t worker;
assert(pthread_create(&worker, NULL, run_reindex_worker, work) == 0);
start_test_worker(&worker, run_reindex_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 0);
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
@@ -100,7 +101,7 @@ static void run_uptime_cache_case(MYSQL *mysql) {
poller_thread_t **prior_details = details;
details = owned_details;
pthread_t worker;
assert(pthread_create(&worker, NULL, run_reindex_worker, work) == 0);
start_test_worker(&worker, run_reindex_worker, work);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 0);
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
@@ -217,7 +218,7 @@ static int poll_reindex_result(MYSQL *mysql, int action, const char *argument,
poller_thread_t **prior_details = details;
details = owned_details;
pthread_t worker;
assert(pthread_create(&worker, NULL, run_reindex_worker, work) == 0);
start_test_worker(&worker, run_reindex_worker, work);
assert(pthread_join(worker, NULL) == 0);
details = prior_details;
free(owned_details);
@@ -318,7 +319,7 @@ void test_reindex_result_contracts(MYSQL *mysql) {
poller_thread_t **prior_details = details;
details = owned_details;
pthread_t worker;
assert(pthread_create(&worker, NULL, run_reindex_worker, work) == 0);
start_test_worker(&worker, run_reindex_worker, work);
assert(pthread_join(worker, NULL) == 0);
details = prior_details;
free(owned_details);
+255
View File
@@ -0,0 +1,255 @@
/* spine_log() stack use and the spine.conf tokenizer.
*
* Poller threads run on the platform's default stack: 128 KiB on musl,
* 512 KiB on macOS. The logging cases run on a thread of the musl size in a
* child process, so an overflow is a reported failure rather than a crash of
* the runner.
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include "common.h"
#include "spine.h"
#define MUSL_DEFAULT_STACK (128 * 1024)
static char log_path[] = "spine-log-stack-XXXXXX";
static void *log_from_thread(void *argument) {
char *message;
(void) argument;
message = malloc(LOGSIZE);
if (message == NULL) return argument;
memset(message, 'x', LOGSIZE - 1);
message[LOGSIZE - 1] = '\0';
set.logging.log_destination = LOGDEST_FILE;
spine_log("stack check %s", message);
set.logging.log_destination = LOGDEST_STDOUT;
spine_log("stack check %s", message);
free(message);
return NULL;
}
static void test_spine_log_fits_a_musl_thread_stack(void **state) {
pid_t child;
int status;
int fd;
(void) state;
fd = mkstemp(log_path);
assert_true(fd >= 0);
assert_int_equal(close(fd), 0);
fflush(NULL);
child = fork();
assert_true(child >= 0);
if (child == 0) {
pthread_attr_t attr;
pthread_t thread;
void *result = &status;
/* cmocka traps SIGSEGV; let an overflow kill the child visibly */
signal(SIGSEGV, SIG_DFL);
signal(SIGBUS, SIG_DFL);
if (freopen("/dev/null", "w", stdout) == NULL) _exit(3);
memset(&set, 0, sizeof(set));
config_defaults();
set.logging.log_level = POLLER_VERBOSITY_LOW;
set.logging.logfile_processed = TRUE;
set.console.stdout_notty = FALSE;
set.console.stderr_notty = TRUE;
STRNCOPY(set.logging.path_logfile, log_path);
if (pthread_attr_init(&attr) != 0) _exit(4);
if (pthread_attr_setstacksize(&attr, MUSL_DEFAULT_STACK) != 0) _exit(5);
if (pthread_create(&thread, &attr, log_from_thread, NULL) != 0) _exit(6);
if (pthread_join(thread, &result) != 0 || result != NULL) _exit(7);
_exit(0);
}
assert_int_equal(waitpid(child, &status, 0), child);
unlink(log_path);
if (!WIFEXITED(status)) {
fail_msg("spine_log() killed a 128 KiB thread with signal %d", WTERMSIG(status));
}
assert_int_equal(WEXITSTATUS(status), 0);
}
/* --- spine.conf parsing -------------------------------------------------- */
static char config_path[64];
static void write_config(const char *contents) {
FILE *fp;
int fd;
strcpy(config_path, "spine-conf-test-XXXXXX");
fd = mkstemp(config_path);
assert_true(fd >= 0);
fp = fdopen(fd, "w");
assert_non_null(fp);
assert_true(fputs(contents, fp) != EOF);
assert_int_equal(fclose(fp), 0);
}
static int config_setup(void **state) {
(void) state;
memset(&set, 0, sizeof(set));
config_defaults();
set.console.stdout_notty = TRUE;
set.console.stderr_notty = TRUE;
config_path[0] = '\0';
return 0;
}
static int config_teardown(void **state) {
(void) state;
if (config_path[0] != '\0') unlink(config_path);
return 0;
}
/* sscanf("%15s") cut a long key short and handed its tail over as the value,
* so a misspelt key could set a real one. */
static void test_config_long_key_is_not_truncated_into_a_known_key(void **state) {
(void) state;
write_config("SNMP_ClientaddrX 10.9.9.9\nDB_Port 3311\n");
assert_int_equal(read_spine_config(config_path), 0);
assert_string_equal(set.snmp.snmp_clientaddr, "");
assert_int_equal(set.database.port, 3311);
}
static void test_config_every_known_key_is_accepted(void **state) {
static const char *const keys[] = {
"RDB_Host", "RDB_Database", "RDB_User", "RDB_Pass", "RDB_SSL_Key",
"RDB_SSL_Cert", "RDB_SSL_CA", "DB_Host", "DB_Database", "DB_User",
"DB_Pass", "DB_SSL_Key", "DB_SSL_Cert", "DB_SSL_CA", "SNMP_Clientaddr",
"RDB_Port", "RDB_UseSSL", "DB_Port", "DB_UseSSL", "Poller", "Cacti_Log",
};
char contents[2048];
char *cursor = contents;
size_t remaining = sizeof(contents);
size_t i;
(void) state;
for (i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
assert_true(spine_appendf(&cursor, &remaining, "%s 1\n", keys[i]));
}
write_config(contents);
assert_int_equal(read_spine_config(config_path), 0);
assert_string_equal(set.snmp.snmp_clientaddr, "1");
assert_string_equal(set.remote_database.ssl_ca, "1");
assert_int_equal(set.remote_database.ssl, 1);
assert_int_equal(set.poller.poller_id, 1);
}
/* %255s cut a long certificate path, and the shortened path was used. */
static void test_config_long_value_is_kept_whole(void **state) {
char contents[BUFSIZE];
char path[700];
(void) state;
memset(path, 'p', sizeof(path) - 1);
path[0] = '/';
path[sizeof(path) - 1] = '\0';
snprintf(contents, sizeof(contents), "DB_SSL_CA %s\n", path);
write_config(contents);
assert_int_equal(read_spine_config(config_path), 0);
assert_string_equal(set.database.ssl_ca, path);
}
/* A line longer than the read buffer came back from fgets() in pieces, and
* the tail was parsed as a directive of its own. */
static void test_config_overlong_line_is_ignored_whole(void **state) {
char *contents;
size_t used;
(void) state;
contents = malloc(4 * BUFSIZE);
assert_non_null(contents);
strcpy(contents, "# ");
used = strlen(contents);
memset(contents + used, 'c', BUFSIZE - 1 - used);
used = BUFSIZE - 1;
strcpy(contents + used, "DB_Host injected\nDB_Port 3312\n");
write_config(contents);
free(contents);
assert_int_equal(read_spine_config(config_path), 0);
assert_string_equal(set.database.host, DEFAULT_DB_HOST);
assert_int_equal(set.database.port, 3312);
}
static void test_config_line_filling_the_buffer_exactly_is_read(void **state) {
char contents[2 * BUFSIZE];
char value[BUFSIZE];
size_t length;
(void) state;
/* "DB_User " plus the value is BUFSIZE - 1 characters before the newline */
length = BUFSIZE - 1 - strlen("DB_User ");
memset(value, 'u', length);
value[length] = '\0';
snprintf(contents, sizeof(contents), "DB_User %s\nDB_Port 3313\n", value);
write_config(contents);
assert_int_equal(read_spine_config(config_path), 0);
assert_string_equal(set.database.user, value);
assert_int_equal(set.database.port, 3313);
}
/* strchr() stopped at the NUL, so the line looked unfinished and the next
* one was thrown away as its remainder. */
static void test_config_embedded_nul_does_not_swallow_the_next_line(void **state) {
static const char contents[] = "DB_Host a\0junk\nDB_Port 3307\n";
FILE *fp;
int fd;
(void) state;
strcpy(config_path, "spine-conf-test-XXXXXX");
fd = mkstemp(config_path);
assert_true(fd >= 0);
fp = fdopen(fd, "w");
assert_non_null(fp);
assert_int_equal(fwrite(contents, 1, sizeof(contents) - 1, fp), sizeof(contents) - 1);
assert_int_equal(fclose(fp), 0);
assert_int_equal(read_spine_config(config_path), 0);
assert_string_equal(set.database.host, "a");
assert_int_equal(set.database.port, 3307);
}
static void test_config_last_line_without_newline_is_read(void **state) {
(void) state;
write_config("DB_Host first\nDB_Port 3399");
assert_int_equal(read_spine_config(config_path), 0);
assert_string_equal(set.database.host, "first");
assert_int_equal(set.database.port, 3399);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_spine_log_fits_a_musl_thread_stack),
cmocka_unit_test_setup_teardown(test_config_long_key_is_not_truncated_into_a_known_key, config_setup, config_teardown),
cmocka_unit_test_setup_teardown(test_config_every_known_key_is_accepted, config_setup, config_teardown),
cmocka_unit_test_setup_teardown(test_config_long_value_is_kept_whole, config_setup, config_teardown),
cmocka_unit_test_setup_teardown(test_config_overlong_line_is_ignored_whole, config_setup, config_teardown),
cmocka_unit_test_setup_teardown(test_config_line_filling_the_buffer_exactly_is_read, config_setup, config_teardown),
cmocka_unit_test_setup_teardown(test_config_embedded_nul_does_not_swallow_the_next_line, config_setup, config_teardown),
cmocka_unit_test_setup_teardown(test_config_last_line_without_newline_is_read, config_setup, config_teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+535
View File
@@ -0,0 +1,535 @@
/* Reachability verdicts and timing in ping.c.
*
* Each case drives the shipped probe through controlled socket calls, so a
* lost packet, a late reply or an ICMP error arrives exactly when the case
* needs it. Nothing here needs root, raw sockets or a route off the host.
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "common.h"
#include "spine.h"
#include "ping.h"
extern int *debug_devices;
static int pt_debug_table[100];
static int controlled_pair[2] = {-1, -1};
static int use_controlled_socket;
static int icmp6_mode;
static int sends;
static int drop_sends;
static int fake_select_timeouts;
static struct timeval first_select_timeout;
static int select_calls;
static unsigned char last_request[64];
static size_t last_request_length;
static int recv_errno;
static int recv_errno_limit;
static int recv_calls;
static int test_socket(int domain, int type, int protocol);
static ssize_t test_sendto(int fd, const void *buffer, size_t length, int flags,
const struct sockaddr *address, socklen_t address_len);
static int test_select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
static ssize_t test_recvfrom(int fd, void *buffer, size_t length, int flags,
struct sockaddr *address, socklen_t *address_len);
static ssize_t test_recv(int fd, void *buffer, size_t length, int flags);
#define socket test_socket
#define sendto test_sendto
#define select test_select
#define recvfrom test_recvfrom
#define recv test_recv
#include "../../ping.c"
#undef socket
#undef sendto
#undef select
#undef recvfrom
#undef recv
static int test_socket(int domain, int type, int protocol) {
if (use_controlled_socket && type != SOCK_STREAM && protocol != IPPROTO_UDP) {
return controlled_pair[0];
}
return socket(domain, type, protocol);
}
static ssize_t test_sendto(int fd, const void *buffer, size_t length, int flags,
const struct sockaddr *address, socklen_t address_len) {
if (!use_controlled_socket || fd != controlled_pair[0]) {
return sendto(fd, buffer, length, flags, address, address_len);
}
if (length > sizeof(last_request)) {
errno = EMSGSIZE;
return -1;
}
memcpy(last_request, buffer, length);
last_request_length = length;
sends++;
return (ssize_t) length;
}
/* The first drop_sends probes are lost: their wait runs the real select()
* on a quiet socket, so the clock moves exactly as it would on the wire.
* Later probes are answered at once. */
static int test_select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout) {
if (!use_controlled_socket) {
return select(nfds, readfds, writefds, exceptfds, timeout);
}
if (select_calls++ == 0 && timeout != NULL) {
first_select_timeout = *timeout;
}
if (fake_select_timeouts) {
return 0;
}
if (sends <= drop_sends) {
return select(nfds, readfds, writefds, exceptfds, timeout);
}
return 1;
}
static ssize_t test_recvfrom(int fd, void *buffer, size_t length, int flags,
struct sockaddr *address, socklen_t *address_len) {
unsigned char *reply = buffer;
if (!use_controlled_socket || fd != controlled_pair[0]) {
return recvfrom(fd, buffer, length, flags, address, address_len);
}
if (sends <= drop_sends || fake_select_timeouts) {
errno = EAGAIN;
return -1;
}
/* Echo the probe back as the reply a datagram ICMP socket delivers. */
assert_true(length >= last_request_length);
memcpy(reply, last_request, last_request_length);
#ifdef SPINE_HAVE_ICMPV6
if (icmp6_mode) {
struct sockaddr_in6 *source = (struct sockaddr_in6 *) address;
reply[0] = ICMP6_ECHO_REPLY;
assert_true(*address_len >= sizeof(*source));
memset(source, 0, sizeof(*source));
source->sin6_family = AF_INET6;
source->sin6_addr = in6addr_loopback;
*address_len = sizeof(*source);
} else
#endif
{
struct sockaddr_in *source = (struct sockaddr_in *) address;
reply[0] = ICMP_ECHOREPLY;
assert_true(*address_len >= sizeof(*source));
memset(source, 0, sizeof(*source));
source->sin_family = AF_INET;
source->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
*address_len = sizeof(*source);
}
return (ssize_t) last_request_length;
}
/* Real recv(), then the errno a connected UDP socket would report if the
* matching ICMP error had arrived instead of the datagram. */
static ssize_t test_recv(int fd, void *buffer, size_t length, int flags) {
ssize_t received = recv(fd, buffer, length, flags);
recv_calls++;
if (received >= 0 && recv_errno != 0 &&
(recv_errno_limit == 0 || recv_calls <= recv_errno_limit)) {
errno = recv_errno;
return -1;
}
return received;
}
static void make_host(host_t *host, const char *address, int method) {
memset(host, 0, sizeof(*host));
host->id = 1;
snprintf(host->hostname, sizeof(host->hostname), "%s", address);
host->availability.timeout = 300;
host->availability.retries = 1;
host->availability.method = AVAIL_PING;
host->availability.ping_method = method;
}
static int timing_setup(void **state) {
(void) state;
config_defaults();
init_mutexes();
memset(pt_debug_table, 0, sizeof(pt_debug_table));
debug_devices = pt_debug_table;
set.availability.icmp_uses_caps = TRUE;
use_controlled_socket = 0;
icmp6_mode = 0;
sends = 0;
drop_sends = 0;
fake_select_timeouts = 0;
select_calls = 0;
memset(&first_select_timeout, 0, sizeof(first_select_timeout));
last_request_length = 0;
recv_errno = 0;
recv_errno_limit = 0;
recv_calls = 0;
return 0;
}
static int timing_teardown(void **state) {
(void) state;
if (controlled_pair[0] != -1) close(controlled_pair[0]);
if (controlled_pair[1] != -1) close(controlled_pair[1]);
controlled_pair[0] = -1;
controlled_pair[1] = -1;
return 0;
}
static void use_controlled_icmp_socket(void) {
assert_int_equal(socketpair(AF_UNIX, SOCK_DGRAM, 0, controlled_pair), 0);
use_controlled_socket = 1;
}
/* --- G4: one lost probe must not use up the retry budget ------------------ */
static void test_icmp_retry_after_one_lost_probe_is_alive(void **state) {
host_t host;
ping_t ping;
(void) state;
use_controlled_icmp_socket();
drop_sends = 1;
make_host(&host, "127.0.0.1", PING_ICMP);
memset(&ping, 0, sizeof(ping));
assert_int_equal(ping_icmp(&host, &ping), HOST_UP);
assert_int_equal(sends, 2);
assert_string_equal(ping.ping_response, "ICMP: Device is Alive");
/* the reported time is the round trip of the answered probe, not the
* time since the first one was lost */
assert_true(atof(ping.ping_status) < host.availability.timeout);
}
#ifdef SPINE_HAVE_ICMPV6
static void test_icmpv6_retry_after_one_lost_probe_is_alive(void **state) {
host_t host;
ping_t ping;
(void) state;
use_controlled_icmp_socket();
icmp6_mode = 1;
drop_sends = 1;
make_host(&host, "::1", PING_ICMP);
memset(&ping, 0, sizeof(ping));
assert_int_equal(ping_icmp_ipv6(&host, &ping), HOST_UP);
assert_int_equal(sends, 2);
assert_string_equal(ping.ping_response, "ICMPv6: Device is Alive");
assert_true(atof(ping.ping_status) < host.availability.timeout);
}
#endif
/* --- G8: a 1500 ms timeout waits 1500 ms, not 2500 ------------------------ */
static long timeval_usec(const struct timeval *tv) {
return (long) tv->tv_sec * 1000000L + (long) tv->tv_usec;
}
static void test_wait_left_converts_without_rounding_up(void **state) {
static const double offsets[] = {0.25, 0.5, 0.75, 1.4, 1.5, 1.6, 9.999};
struct timeval tv;
size_t i;
(void) state;
for (i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) {
long expected = (long) (offsets[i] * 1000000.0);
ping_wait_left(spine_monotonic_time() + offsets[i], &tv);
assert_true(tv.tv_usec >= 0 && tv.tv_usec < 1000000);
assert_true(timeval_usec(&tv) <= expected);
assert_true(timeval_usec(&tv) > expected - 50000L);
}
/* a deadline already behind us polls instead of waiting */
ping_wait_left(spine_monotonic_time() - 1.0, &tv);
assert_int_equal(timeval_usec(&tv), 0);
}
static void test_icmp_wait_matches_the_device_timeout(void **state) {
host_t host;
ping_t ping;
long waited;
(void) state;
use_controlled_icmp_socket();
fake_select_timeouts = 1;
make_host(&host, "127.0.0.1", PING_ICMP);
host.availability.timeout = 1500;
host.availability.retries = 0;
memset(&ping, 0, sizeof(ping));
assert_int_equal(ping_icmp(&host, &ping), HOST_DOWN);
waited = timeval_usec(&first_select_timeout);
assert_true(waited <= 1500000L);
assert_true(waited > 1400000L);
}
#ifdef SPINE_HAVE_ICMPV6
static void test_icmpv6_wait_matches_the_device_timeout(void **state) {
host_t host;
ping_t ping;
long waited;
(void) state;
use_controlled_icmp_socket();
icmp6_mode = 1;
fake_select_timeouts = 1;
make_host(&host, "::1", PING_ICMP);
host.availability.timeout = 1500;
host.availability.retries = 0;
memset(&ping, 0, sizeof(ping));
assert_int_equal(ping_icmp_ipv6(&host, &ping), HOST_DOWN);
waited = timeval_usec(&first_select_timeout);
assert_true(waited <= 1500000L);
assert_true(waited > 1400000L);
}
#endif
/* --- G6: what a UDP probe may take as proof of life ----------------------- */
typedef struct {
int fd;
int reply;
int probes;
} udp_peer_t;
/* Answers one probe, which is what makes the client socket readable. The
* injected errno then decides what that readability meant. */
static void *udp_peer(void *argument) {
udp_peer_t *peer = argument;
struct sockaddr_storage client;
socklen_t length = sizeof(client);
char buffer[64];
ssize_t received;
int probes;
/* shutdown() does not wake a parked recvfrom() on every platform, so the
* peer waits for exactly the probes the client will send */
for (probes = 0; probes < peer->probes; probes++) {
length = sizeof(client);
received = recvfrom(peer->fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client, &length);
if (received <= 0) {
break;
}
if (peer->reply) {
sendto(peer->fd, "pong", 4, 0, (struct sockaddr *) &client, length);
}
}
return NULL;
}
static int run_udp_probe_retries(int injected_errno, int reply, int retries, ping_t *ping) {
struct sockaddr_in address;
socklen_t length = sizeof(address);
udp_peer_t peer;
pthread_t thread;
host_t host;
int rc;
peer.fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
assert_true(peer.fd >= 0);
peer.reply = reply;
peer.probes = retries + 1;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
assert_int_equal(bind(peer.fd, (struct sockaddr *) &address, sizeof(address)), 0);
assert_int_equal(getsockname(peer.fd, (struct sockaddr *) &address, &length), 0);
assert_int_equal(pthread_create(&thread, NULL, udp_peer, &peer), 0);
make_host(&host, "127.0.0.1", PING_UDP);
host.availability.port = ntohs(address.sin_port);
host.availability.retries = retries;
host.availability.timeout = 500;
recv_errno = injected_errno;
memset(ping, 0, sizeof(*ping));
rc = ping_udp(&host, ping);
/* a peer still parked in recvfrom() would hang the join */
shutdown(peer.fd, SHUT_RDWR);
assert_int_equal(pthread_join(thread, NULL), 0);
close(peer.fd);
return rc;
}
static int run_udp_probe(int injected_errno, int reply, ping_t *ping) {
return run_udp_probe_retries(injected_errno, reply, 0, ping);
}
/* EHOSTUNREACH includes ordinary unreachable hosts and failed neighbour
* resolution; it is not evidence that the device responded. */
static void test_udp_host_unreachable_is_down(void **state) {
ping_t ping;
(void) state;
assert_int_equal(run_udp_probe(EHOSTUNREACH, 1, &ping), HOST_DOWN);
assert_true(recv_calls >= 1);
assert_string_equal(ping.ping_response, "UDP: Ping timed out");
}
/* One unreachable report, for instance while a neighbour entry resolves, uses
* up an attempt but must not stop the retries that follow. */
static void test_udp_unreachable_then_reply_is_alive(void **state) {
ping_t ping;
(void) state;
recv_errno_limit = 1;
assert_int_equal(run_udp_probe_retries(EHOSTUNREACH, 1, 1, &ping), HOST_UP);
assert_string_equal(ping.ping_response, "UDP: Device is Alive");
}
static void test_udp_network_unreachable_is_down(void **state) {
ping_t ping;
(void) state;
assert_int_equal(run_udp_probe(ENETUNREACH, 1, &ping), HOST_DOWN);
assert_true(recv_calls >= 1);
}
#ifdef EHOSTDOWN
static void test_udp_host_down_is_down(void **state) {
ping_t ping;
(void) state;
assert_int_equal(run_udp_probe(EHOSTDOWN, 1, &ping), HOST_DOWN);
assert_true(recv_calls >= 1);
}
#endif
static void test_udp_port_unreachable_is_alive(void **state) {
ping_t ping;
(void) state;
assert_int_equal(run_udp_probe(ECONNREFUSED, 1, &ping), HOST_UP);
assert_string_equal(ping.ping_response, "UDP: Device is Alive");
}
static void test_udp_data_reply_is_alive(void **state) {
ping_t ping;
(void) state;
assert_int_equal(run_udp_probe(0, 1, &ping), HOST_UP);
assert_string_equal(ping.ping_response, "UDP: Device is Alive");
}
/* --- G9: transport-qualified and bracketed IPv6 device names -------------- */
static void test_namebyhost_table(void **state) {
static const struct {
const char *input;
const char *hostname;
int method;
int port;
} cases[] = {
{"router.example", "router.example", 0, 0},
{"router.example:161", "router.example", 0, 161},
{"TCP:router.example:443", "router.example", 1, 443},
{"udp:router.example:53", "router.example", 2, 53},
{"tcp6:router.example:22", "router.example", 3, 22},
{"192.0.2.7", "192.0.2.7", 0, 0},
{"2001:db8::1", "2001:db8::1", 0, 0},
{"fe80::1%eth0", "fe80::1%eth0", 0, 0},
{"[2001:db8::1]", "[2001:db8::1]", 0, 0},
{"[::1]:161", "[::1]:161", 0, 161},
{"udp6:[2001:db8::1]:161", "udp6:[2001:db8::1]:161", 4, 161},
{"tcp6:[::1]:22", "tcp6:[::1]:22", 3, 22},
{"UDP6:[::1]", "UDP6:[::1]", 4, 0},
{"udp6:[::1]:notaport", "udp6:[::1]:notaport", 4, 0},
};
size_t i;
(void) state;
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
name_t *name = get_namebyhost(cases[i].input, NULL);
assert_non_null(name);
if (strcmp(name->hostname, cases[i].hostname) != 0 ||
name->method != cases[i].method || name->port != cases[i].port) {
fail_msg("get_namebyhost(\"%s\") gave {\"%s\", %d, %d}, expected {\"%s\", %d, %d}",
cases[i].input, name->hostname, name->method, name->port,
cases[i].hostname, cases[i].method, cases[i].port);
}
free(name);
}
}
#ifdef SPINE_HAVE_ICMPV6
static void test_icmpv6_resolves_transport_qualified_names(void **state) {
static const char *const names[] = {
"udp6:[::1]:161", "tcp6:[::1]:22", "[::1]:161", "[::1]", "UDP6:[::1]", "::1"
};
struct sockaddr_in6 address;
size_t i;
(void) state;
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
memset(&address, 0, sizeof(address));
if (!init_sockaddr6(&address, names[i])) {
fail_msg("init_sockaddr6(\"%s\") failed", names[i]);
}
assert_memory_equal(&address.sin6_addr, &in6addr_loopback, sizeof(struct in6_addr));
}
}
static void test_icmpv6_pings_a_transport_qualified_name(void **state) {
host_t host;
ping_t ping;
(void) state;
use_controlled_icmp_socket();
icmp6_mode = 1;
make_host(&host, "udp6:[::1]:161", PING_ICMP);
memset(&ping, 0, sizeof(ping));
assert_int_equal(ping_icmp_ipv6(&host, &ping), HOST_UP);
}
#endif
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_icmp_retry_after_one_lost_probe_is_alive, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_wait_left_converts_without_rounding_up, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_icmp_wait_matches_the_device_timeout, timing_setup, timing_teardown),
#ifdef SPINE_HAVE_ICMPV6
cmocka_unit_test_setup_teardown(test_icmpv6_retry_after_one_lost_probe_is_alive, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_icmpv6_wait_matches_the_device_timeout, timing_setup, timing_teardown),
#endif
cmocka_unit_test_setup_teardown(test_udp_host_unreachable_is_down, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_udp_network_unreachable_is_down, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_udp_unreachable_then_reply_is_alive, timing_setup, timing_teardown),
#ifdef EHOSTDOWN
cmocka_unit_test_setup_teardown(test_udp_host_down_is_down, timing_setup, timing_teardown),
#endif
cmocka_unit_test_setup_teardown(test_udp_port_unreachable_is_alive, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_udp_data_reply_is_alive, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_namebyhost_table, timing_setup, timing_teardown),
#ifdef SPINE_HAVE_ICMPV6
cmocka_unit_test_setup_teardown(test_icmpv6_resolves_transport_qualified_names, timing_setup, timing_teardown),
cmocka_unit_test_setup_teardown(test_icmpv6_pings_a_transport_qualified_name, timing_setup, timing_teardown),
#endif
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+112 -20
View File
@@ -177,6 +177,34 @@ int spine_permits_available(spine_permits_t *permits) {
return available;
}
/* Raise, never lower, the platform default: glibc already gives 8 MiB, and a
* smaller stack there would only add risk in code nobody has measured. */
int spine_thread_attr_init(pthread_attr_t *attributes) {
size_t current = 0;
size_t wanted = SPINE_THREAD_STACK_SIZE;
int status;
#ifdef PTHREAD_STACK_MIN
if (wanted < (size_t) PTHREAD_STACK_MIN) wanted = (size_t) PTHREAD_STACK_MIN;
#endif
status = pthread_attr_init(attributes);
if (status != 0) {
return status;
}
if (pthread_attr_getstacksize(attributes, &current) == 0 && current >= wanted) {
return 0;
}
status = pthread_attr_setstacksize(attributes, wanted);
if (status != 0) {
pthread_attr_destroy(attributes);
}
return status;
}
void spine_clear_sensitive(void *buffer, size_t length) {
volatile unsigned char *bytes = buffer;
while (length > 0) {
@@ -1317,6 +1345,21 @@ static int apply_config_directive(const char *name, const char *value) {
return TRUE;
}
/* fgets() returns a line longer than its buffer in pieces, and each piece
* would be parsed as a directive of its own. Report whether the line just
* read was whole, consuming the remainder when it was not. The caller sets
* the last byte nonzero before reading: fgets() only clears it when it filled
* the buffer, which a string search cannot tell once the line holds a NUL. */
static bool config_line_complete(FILE *fp, const char *line, size_t capacity) {
int next;
if (line[capacity - 1] != '\0' || line[capacity - 2] == '\n') return TRUE;
next = getc(fp);
if (next == EOF || next == '\n') return TRUE;
while (next != EOF && next != '\n') next = getc(fp);
return FALSE;
}
int read_spine_config(const char *file) {
FILE *fp = fopen(file, "rb");
char buff[BUFSIZE];
@@ -1324,7 +1367,7 @@ int read_spine_config(const char *file) {
char value[BUFSIZE];
char display_file[BUFSIZE];
int line = 0;
bool line_start = TRUE;
int fields;
strncopy(display_file, file, sizeof(display_file));
spine_sanitize_log_message(display_file);
@@ -1337,14 +1380,31 @@ int read_spine_config(const char *file) {
if (!set.console.stdout_notty) {
fprintf(stdout, "SPINE: Using spine config file [%s]\n", display_file);
}
while (fgets(buff, sizeof(buff), fp) != NULL) {
/* A line longer than buff arrives in several pieces; count it once. */
if (line_start) line++;
line_start = strchr(buff, '\n') != NULL;
for (;;) {
buff[sizeof(buff) - 1] = 1;
if (fgets(buff, sizeof(buff), fp) == NULL) break;
/* config_line_complete() consumes an overlong line whole, so each
* pass is exactly one line */
line++;
if (!config_line_complete(fp, buff, sizeof(buff))) {
if (!set.console.stderr_notty) {
fprintf(stderr, "WARNING: Ignoring line %d of %s, longer than %d characters\n", line, display_file, BUFSIZE - 2);
}
continue;
}
if (buff[0] == '#' || buff[0] == ' ' || buff[0] == '\n') continue;
if (sscanf(buff, "%15s %255s", name, value) != 2) continue;
/* -C accepts any path, so report where the line is, never what it holds. */
if (!apply_config_directive(name, value) && !set.console.stderr_notty) {
/* Field widths match the line buffer, so no token is ever cut; the old
* %15s handed the tail of a long key over as its value. */
fields = sscanf(buff, "%1023s %1023s", name, value);
if (fields < 1) continue;
/* -C accepts any path, so report where the line is, never what it holds.
* A lone token is a key without a value, or a line that is no
* directive at all; the old %15s split such a line in two. */
if (fields == 1) {
if (!set.console.stderr_notty) {
fprintf(stderr, "WARNING: Directive without a value on line %d of %s\n", line, display_file);
}
} else if (!apply_config_directive(name, value) && !set.console.stderr_notty) {
fprintf(stderr, "WARNING: Unrecognized directive on line %d of %s\n", line, display_file);
}
}
@@ -1489,8 +1549,8 @@ static void log_format_error(const char *message) {
if (log_stream_available(stream)) fprintf(stream, "%s\n", message);
}
static bool log_format_message(char *output, const char *message) {
char prefix[LOGSIZE];
static bool log_format_message(char *output, size_t capacity, const char *message) {
char prefix[SMALL_BUFSIZE];
snprintf(prefix, sizeof(prefix), "SPINE: Poller[%i] PID[%i] PT[%lu] ", set.poller.poller_id, getpid(), (unsigned long int)pthread_self());
time_t now = time(NULL);
struct tm local;
@@ -1503,10 +1563,10 @@ static bool log_format_message(char *output, const char *message) {
}
int prefix_length = spine_count_to_int(strlen(prefix));
int message_length = spine_count_to_int(strlen(message));
int available = LOGSIZE - spine_count_to_int(date_length) - 2;
int available = spine_count_to_int(capacity) - spine_count_to_int(date_length) - 2;
if (prefix_length > available) prefix_length = available;
if (message_length > available - prefix_length) message_length = available - prefix_length;
snprintf(output + date_length, LOGSIZE - date_length, "%.*s%.*s", prefix_length, prefix, message_length, message);
snprintf(output + date_length, capacity - date_length, "%.*s%.*s", prefix_length, prefix, message_length, message);
return date_length != 0;
}
@@ -1548,24 +1608,56 @@ static bool log_to_file(const char *message) {
return success;
}
int spine_log(const char *format, ...) {
/* Three LOGSIZE records on the stack overran the 128 KiB musl default thread
* stack. Each thread takes its buffers from the heap on its first message
* and keeps them until it exits; if that allocation fails the record is
* still written, cut to BUFSIZE. */
typedef struct {
char message[LOGSIZE];
char formatted[LOGSIZE];
} log_buffers_t;
static pthread_key_t log_buffers_key;
static pthread_once_t log_buffers_once = PTHREAD_ONCE_INIT;
static bool log_buffers_keyed = FALSE;
static void log_buffers_create_key(void) {
log_buffers_keyed = pthread_key_create(&log_buffers_key, free) == 0;
}
static log_buffers_t *log_buffers(void) {
log_buffers_t *buffers;
if (pthread_once(&log_buffers_once, log_buffers_create_key) != 0 || !log_buffers_keyed) return NULL;
buffers = pthread_getspecific(log_buffers_key);
if (buffers != NULL) return buffers;
buffers = malloc(sizeof(*buffers));
if (buffers != NULL && pthread_setspecific(log_buffers_key, buffers) != 0) {
free(buffers);
buffers = NULL;
}
return buffers;
}
int spine_log(const char *format, ...) {
char fallback_message[BUFSIZE];
char fallback_formatted[BUFSIZE];
log_buffers_t *buffers = log_buffers();
char *message = buffers != NULL ? buffers->message : fallback_message;
char *formatted = buffers != NULL ? buffers->formatted : fallback_formatted;
size_t capacity = buffers != NULL ? LOGSIZE : BUFSIZE;
va_list args;
va_start(args, format);
vsnprintf(message, LOGSIZE - 1, format, args);
vsnprintf(message, capacity - 1, format, args);
va_end(args);
spine_sanitize_log_message(message);
if (IS_LOGGING_TO_STDOUT()) {
char console[LOGSIZE + 20];
snprintf(console, sizeof(console), "Total[%3.4f] %s", get_time_as_double() - start_time, message);
return puts(console) == EOF ? FALSE : TRUE;
return printf("Total[%3.4f] %s\n", get_time_as_double() - start_time, message) < 0 ? FALSE : TRUE;
}
char formatted[LOGSIZE];
bool date_valid = log_format_message(formatted, message);
bool date_valid = log_format_message(formatted, capacity, message);
log_to_syslog(formatted);
if (strchr(formatted, '\n') == NULL) {
size_t used = strlen(formatted);
snprintf(formatted + used, sizeof(formatted) - used, "\n");
snprintf(formatted + used, capacity - used, "\n");
}
bool success = log_to_file(formatted);
if (set.logging.log_level >= POLLER_VERBOSITY_NONE) {
+22
View File
@@ -158,6 +158,28 @@ extern int spine_permits_destroy(spine_permits_t *permits);
extern int spine_permits_try_acquire(spine_permits_t *permits);
extern int spine_permits_release(spine_permits_t *permits);
extern int spine_permits_available(spine_permits_t *permits);
/* Stack for every thread spine creates. The deepest worker path, child()
* through poll_host() into the SNMP, script, ping and logging code, peaked at
* 179,024 bytes (glibc, gcc ASan, live MariaDB and snmpd) and sums to 175,856
* bytes statically (gcc 15 -fcallgraph-info=su on musl). About five results
* buffers of that grow with --with-results-buffer. musl's 128 KiB default
* overflows on entry to poll_host(). Reserve twice 180 KiB plus five results
* buffers, rounded up to 64 KiB so the size is a page multiple everywhere:
* 384 KiB with the default 2048-byte results buffer. */
/* Check the unrounded expression before evaluating its size_t arithmetic. */
_Static_assert(RESULTS_BUFFER >= 2, "RESULTS_BUFFER must hold a value and terminator");
_Static_assert((size_t) RESULTS_BUFFER <= (SIZE_MAX - ((size_t) 2 * 180 * 1024 + 65535)) / 10,
"RESULTS_BUFFER overflows the worker stack size");
#define SPINE_THREAD_STACK_SIZE \
(((size_t) 2 * ((size_t) 180 * 1024 + 5 * (size_t) RESULTS_BUFFER) + 65535) & ~(size_t) 65535)
/*! \fn int spine_thread_attr_init(pthread_attr_t *attributes)
* \brief initialize thread attributes with at least SPINE_THREAD_STACK_SIZE
* \return 0, or the pthread error; on error nothing is left to destroy
*/
extern int spine_thread_attr_init(pthread_attr_t *attributes);
/*! \fn int spine_appendf(char **cursor, size_t *remaining, const char *fmt, ...)
* \brief append to a bounded buffer without walking off the end
*