mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
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:
1 parent
8e741b36cd
commit
9582b71704
15 files changed
+1622
-113
No files matched your search
+15
-2
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(®ex_worker, NULL, test_poll_worker, work) == 0);
|
||||
start_test_worker(®ex_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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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, ¤t) == 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) {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user