Skip to content

fix(ping): make IPv4 and IPv6 ICMP polling safe - #619

Open
somethingwithproof wants to merge 19 commits into
Cacti:developfrom
somethingwithproof:fix/597-icmp-resolver-safety
Open

fix(ping): make IPv4 and IPv6 ICMP polling safe#619
somethingwithproof wants to merge 19 commits into
Cacti:developfrom
somethingwithproof:fix/597-icmp-resolver-safety

Conversation

@somethingwithproof

@somethingwithproof somethingwithproof commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

Consolidates the overlapping ICMP work from #555 and #619 into one review of the complete socket and resolver state machine.

  • add ICMPv6 echo polling and dispatch IPv6 devices to it
  • prefer unprivileged datagram ICMP sockets when the kernel permits them
  • retain bounded raw-socket fallback for IPv4 and IPv6
  • give packet and socket resources one cleanup owner
  • leave caller-owned resolver state untouched on lookup failure
  • keep every retry inside one cached capability and privilege-transition decision
  • prevent LOCK_SETEUID retry deadlocks
  • validate raw and datagram reply layouts, source, sequence, identifiers, and payload
  • retain fuzz and unprivileged-socket integration coverage

The consolidation also fixed an integration defect found locally: the controlled successful-reply test constructed only a raw IPv4 packet even when the production path selected SOCK_DGRAM. It now models the selected socket format, and IPv6 raw retries use the same privilege-safety invariant as IPv4.

Verification

  • warning-enabled local build completed with MariaDB Connector/C, Net-SNMP, OpenSSL 3, and cmocka
  • make check: 5/5 test binaries passed
  • focused ping_icmp suite: 9/9 cases passed
  • actionlint and git diff --check passed
  • all commits are signed
  • GitHub CI is authoritative for Linux capability, sanitizer, fuzz, and unprivileged-network coverage

Closes #127.
Closes #250.
Closes #593.
Closes #611.
Supersedes #555.
Refs #597 and #618.

Copilot AI lite review requested due to automatic review settings September 6, 2026 06:02
@somethingwithproof

Copy link
Copy Markdown
Member Author

@copilot review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes privilege/locking behavior in a security-sensitive (SUID-root) ICMP path, which warrants final human audit despite strong unit coverage.

Pull request overview

This PR hardens Spine’s ICMP ping path by making raw-socket privilege handling and resource cleanup deterministic, and by ensuring resolver failures don’t corrupt caller-owned state. It adds a focused unit test suite to cover the previously fragile/error-prone ICMP ownership and retry logic.

Changes:

  • Refactors ping_icmp() to use a single cleanup path and to avoid retry deadlocks by releasing LOCK_SETEUID before sleeping/retrying.
  • Fixes init_sockaddr() to avoid freeing indeterminate getaddrinfo() output by initializing hostinfo and only freeing on success.
  • Introduces set.icmp_uses_caps to cache Linux capability mode decisions and adds a dedicated ICMP unit test binary.
File summaries
File Description
util.c Ensures capability handles are freed on all paths; records whether ICMP uses Linux capabilities.
ping.c Centralizes ICMP resource cleanup, fixes retry deadlock risk around LOCK_SETEUID, and makes resolver failure handling safe.
spine.h Adds icmp_uses_caps config field to track capability-based ICMP operation.
spine.c Initializes set.icmp_uses_caps at startup.
tests/unit/test_ping_icmp.c Adds deterministic ICMP ownership/cleanup/retry/lock-balance unit tests by intercepting sinks.
tests/unit/test_linked.c Adds portability shim for ICMP unreachable constant naming.
Makefile.am Registers the new ICMP unit test program in check_PROGRAMS.
CHANGELOG Documents the ICMP/resolver/capability safety fixes.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

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

Comment thread util.c
@somethingwithproof

Copy link
Copy Markdown
Member Author

@copilot review

A bare IPv6 literal never reached an ICMP socket, so spine reported the
device as unpingable instead of probing it.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Spine asked only for SOCK_RAW and treated ICMP as unavailable whenever it was
neither root nor capability-endowed, so net.ipv4.ping_group_range had no
effect however the sysctl was set. Both the availability probe and the two
ping paths now try SOCK_DGRAM first and fall back to the raw socket, which
also means the IPv4 receive path has to allow for the stripped header and the
IPv6 one for the kernel-assigned echo id. Reported in Cacti#250.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…sifier

The target was dropped between Cacti#542 and Cacti#563: it could not go with the test
split because it calls spine_icmp_classify_reply(), and the rebuild of Cacti#542
carried source files only. An input byte now selects which classifier runs, so
the datagram path added here is fuzzed alongside the raw one.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Six exits each released the two resources on their own terms and had drifted:
the FD_SETSIZE guard closed the socket and returned without freeing the packet,
leaking packet_len bytes per affected device per cycle. Closes Cacti#593.

One cleanup label now. Four frees and five closes become one of each, which is
what makes the next early return safe by construction rather than by review.

Three of those exits also re-entered root and took LOCK_SETEUID to call
close(), which needs no privileges. The raw socket needs root to open, which
happens once above and drops immediately. Removing that shortens the elevated
window and stops every poller thread serialising on a global mutex to close a
descriptor.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The review named this the least-tested and highest-consequence change in the
branch, and it was right: ping_icmp() runs in a SUID-root binary, its five
exits were collapsed onto one cleanup label, and the seteuid(0) wrapper around
close() was removed, with nothing covering any of it.

A raw ICMP socket is available under docker, so the function is reachable. The
tests skip rather than fail where it is not.

The third case drives the exit that leaked: hold descriptors until a new socket
would land at or above FD_SETSIZE, then ping. Against the ping.c from before
ff2c252, LeakSanitizer reports 31 bytes leaked in 1 allocation. Against this
one it is clean. That is Cacti#593 demonstrated rather than argued.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The retry slept and looped back still holding the lock, so the second attempt
relocked a non-recursive process-global mutex from its own owner and wedged the
thread at euid 0 with every other poller thread behind it. Each attempt is now
a complete lock/elevate/open/drop/unlock block, which also narrows the elevated
window to the socket() call.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
getaddrinfo() leaves the result pointer unspecified on error, so the five
failure paths in init_sockaddr() were passing whatever the stack held to
freeaddrinfo(). Initialise it and drop those frees, since nothing was
allocated.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Map the BSD ICMP_UNREACH name to the Linux ICMP_DEST_UNREACH spelling used by the linked-object regression test so the focused branch remains portable.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Keep each IPv6 raw-socket retry inside one cached capability and privilege-transition decision, matching the consolidated IPv4 path.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Build the controlled reply in the format selected by the socket type so the consolidated unprivileged ICMP path exercises its successful cleanup contract.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@somethingwithproof
somethingwithproof force-pushed the fix/597-icmp-resolver-safety branch from 0084b8e to b06dd8e Compare September 6, 2026 07:18
@somethingwithproof somethingwithproof changed the title fix: make ICMP cleanup and retries safe fix(ping): make IPv4 and IPv6 ICMP polling safe Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants