Skip to content

feat(platform): add macOS listener inspector - #301

Merged
munezaclovis merged 4 commits into
mainfrom
agent/macos-listener-inspector
Jul 28, 2026
Merged

feat(platform): add macOS listener inspector#301
munezaclovis merged 4 commits into
mainfrom
agent/macos-listener-inspector

Conversation

@munezaclovis

@munezaclovis munezaclovis commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • introduce the semantic platform::listener facade with private macOS, Linux, Windows, and fallback implementations
  • add an acceptance-only macOS inspector for net.inet.tcp.pcblist_n
  • parse TCP listener records defensively with bounded growth retries, snapshot validation, typed failures, and IPv4/IPv6 loopback and wildcard handling
  • add fixture snapshots and a repeated live four-listener test
  • keep Linux and Windows as typed Unsupported
  • preserve the current production union of netstat-esr and /usr/sbin/netstat

Why

PV needs a narrowly scoped, PV-owned listener inspection capability before it can safely replace the current dual macOS mechanisms. The existing published-library spike proved that named kernel-table lookup is viable, while also exposing IPv6 parsing, stale numeric sysctl, and failure-handling problems that require a purpose-built implementation.

Impact

There is no production listener-inspection behavior change in this PR. The new kernel inspector remains acceptance-only until the supported macOS version and architecture matrix passes. The existing public platform APIs are unchanged.

Validation

  • cargo nextest run -p platform --all-features --locked — 50/50 passed
  • live macOS 26.5.2 arm64 matrix — all four listener classes detected in 10/10 samples
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  • cargo shear
  • cargo fmt --all --check
  • git diff --check

Remaining acceptance gate

Before cutover:

  • run the controlled listener and Apple netstat comparison matrix on every supported macOS version beginning with macOS 13
  • run the matrix on both Intel and Apple Silicon
  • independently review the private-ABI parser, unsafe boundary, typed failure behavior, and race handling
  • remove netstat-esr and /usr/sbin/netstat together only after that evidence passes

Environment notes

A Linux cross-target check was blocked by the local host lacking x86_64-linux-gnu-gcc for C dependencies. A full-workspace nextest attempt compiled the workspace but stalled while macOS loaded the final unrelated test binaries for discovery; the changed platform crate's complete 50-test nextest suite passed.

Summary by CodeRabbit

  • New Features
    • Added macOS support for detecting active TCP listeners on loopback and wildcard addresses.
    • Added APIs to list listening ports and check whether a specific port is active.
  • Bug Fixes
    • Improved macOS listener detection reliability when system network tables change during inspection.
    • Added validation for malformed or inconsistent system network data.
  • Compatibility
    • Listener inspection reports a clear unsupported-capability error on platforms without implementation support.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@munezaclovis, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 164468cf-a930-4c28-a000-0dfd5a9e3a6b

📥 Commits

Reviewing files that changed from the base of the PR and between 99e362b and 5d7a3db.

📒 Files selected for processing (1)
  • crates/platform/src/listener/macos/kernel_table.rs
📝 Walkthrough

Walkthrough

The platform crate adds a listener-inspection abstraction. macOS discovers loopback TCP listeners through kernel table parsing and netstat, while Linux, Windows, and unsupported targets return typed unsupported errors. Public exports and macOS-specific tests are updated accordingly.

Changes

Loopback listener inspection

Layer / File(s) Summary
Listener API and target wiring
crates/platform/src/lib.rs, crates/platform/src/listener.rs, crates/platform/src/listener/{linux,windows,unsupported}.rs
Listener helpers are re-exported from the new module, with OS-specific implementations and macOS-only parser test wiring.
macOS listener orchestration
crates/platform/src/listener/macos.rs, Cargo.toml, crates/platform/Cargo.toml
macOS listener discovery combines socket-table inspection with netstat parsing and adds the target-specific libc dependency.
Kernel TCP table retrieval and parsing
crates/platform/src/listener/macos/kernel_table.rs
Sysctl data is fetched with bounded retries, parsed with typed validation errors, filtered for loopback listeners, and covered by fixture, retry, and live-kernel tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • prvious/pv#253: Established the platform-layer boundary and listener helper exports refactored here.
  • prvious/pv#297: Related target-gated listener inspection and typed unsupported errors.

Poem

A rabbit hops where listeners hide,
Through kernel tables, side by side.
Loopback ports are found with care,
While unsupported paths say, “Not there!”
MacOS nets now neatly flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a macOS listener inspector within the platform crate.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/macos-listener-inspector

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 7 untouched benchmarks


Comparing agent/macos-listener-inspector (5d7a3db) with main (bd272ce)

Open in CodSpeed

@munezaclovis
munezaclovis marked this pull request as ready for review July 28, 2026 02:34

@pullfrog pullfrog Bot 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.

Important

The acceptance-only kernel parser does not yet handle two states emitted by the XNU contract, so it can fail on an empty table or accept an unstable snapshot.

Reviewed changes — This PR introduces a target-specific listener-inspection facade and an acceptance-only macOS kernel-table inspector while preserving the current production macOS inspection path.

  • Add the platform::listener facade — Public listener APIs now dispatch to private macOS, Linux, Windows, or fallback implementations with typed unsupported behavior off macOS.
  • Add private macOS PCB-table inspectionkernel_table.rs fetches net.inet.tcp.pcblist_n, parses bounded private-ABI records, and detects IPv4/IPv6 loopback and wildcard listeners.
  • Preserve production listener detection — macOS continues to union netstat-esr results with /usr/sbin/netstat output while the kernel inspector remains acceptance-only.
  • Cover parser and platform behavior — Synthetic snapshots exercise address families, states, generations, malformed records, and fetch retries; live macOS coverage samples four controlled listener classes.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

Comment thread crates/platform/src/listener/macos/kernel_table.rs Outdated
Comment thread crates/platform/src/listener/macos/kernel_table.rs

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes — The new commit resolves the empty macOS PCB snapshot failure and documents the intended snapshot-coherence semantics.

  • Accepted XNU's empty PCB snapshot shapeparse_tcp_table now returns an empty listener set for a single valid zero-count xinpgen envelope while preserving MissingTrailer for nonzero snapshots.
  • Clarified the generation check — The parser documents why global xig_sogen changes do not invalidate a TCP PCB snapshot, matching XNU and Apple netstat behavior.
  • Added regression coverage — A snapshot fixture covers both zero-count and nonzero single-envelope tables.

Pullfrog  | View workflow run | Using GPT Sol𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/platform/src/listener/macos.rs (1)

24-31: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider making the netstat subprocess a fallback rather than an unconditional second source.

Every call now spawns /usr/sbin/netstat in addition to the in-process socket-table query, and loopback_tcp_port_has_listener funnels through here, so a caller polling a port pays a process spawn per check. Since the netstat pass exists to cover listeners the crate query misses, running it only when the socket table yields nothing keeps the coverage while removing the spawn from the common path.

♻️ Suggested fallback ordering
 pub(super) fn loopback_tcp_listener_ports() -> Result<BTreeSet<u16>, PlatformError> {
-    let mut ports = loopback_tcp_listener_ports_from_socket_table()?;
-    ports.extend(parse_netstat_tcp_listener_ports(
-        &netstat_tcp_socket_table()?
-    ));
-
-    Ok(ports)
+    let ports = loopback_tcp_listener_ports_from_socket_table()?;
+    if !ports.is_empty() {
+        return Ok(ports);
+    }
+
+    Ok(parse_netstat_tcp_listener_ports(
+        &netstat_tcp_socket_table()?,
+    ))
 }

If the two sources are known to disagree in practice, a short comment recording that would justify keeping both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/platform/src/listener/macos.rs` around lines 24 - 31, Update
loopback_tcp_listener_ports to invoke netstat_tcp_socket_table and parse its
results only when loopback_tcp_listener_ports_from_socket_table returns no
ports. Preserve the existing error propagation and merge behavior for the
fallback so missed listeners remain covered, while avoiding the subprocess on
the common non-empty path.
crates/platform/src/listener/macos/kernel_table.rs (1)

637-703: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Build fixtures from the parser's offset constants instead of literals.

18..20, 28..36, 44, 64..80, 76..80, 36..40, and the 24 envelope length restate offsets that already exist as constants. On private-ABI code the fixtures are the main regression guard, so keeping the two copies in sync by hand is the part most likely to drift.

♻️ Suggested wiring
     use super::{
-        FetchError, INP_IPV4, INP_IPV6, MAX_ATTEMPTS, TCPS_LISTEN, XINPCB_MINIMUM_LENGTH,
-        XSO_INPCB, XSO_TCPCB, XTCPCB_MINIMUM_LENGTH, fetch_tcp_table_with,
-        loopback_tcp_listener_ports, parse_tcp_table,
+        FetchError, INP_IPV4, INP_IPV6, INPCB_GENERATION_OFFSET, INPCB_IPV4_ADDRESS_OFFSET,
+        INPCB_LOCAL_ADDRESS_LENGTH, INPCB_LOCAL_ADDRESS_OFFSET, INPCB_LOCAL_PORT_OFFSET,
+        INPCB_VERSION_FLAGS_OFFSET, MAX_ATTEMPTS, TCPCB_STATE_OFFSET, TCPS_LISTEN,
+        XINPCB_MINIMUM_LENGTH, XINPGEN_LENGTH, XSO_INPCB, XSO_TCPCB, XTCPCB_MINIMUM_LENGTH,
+        fetch_tcp_table_with, loopback_tcp_listener_ports, parse_tcp_table,
     };
-            tcp_record[36..40].copy_from_slice(&state.to_ne_bytes());
+            tcp_record[TCPCB_STATE_OFFSET..TCPCB_STATE_OFFSET + 4]
+                .copy_from_slice(&state.to_ne_bytes());
-            internet_record[18..20].copy_from_slice(&port.to_be_bytes());
-            internet_record[28..36].copy_from_slice(&generation.to_ne_bytes());
-            internet_record[44] = version_flags;
+            internet_record[INPCB_LOCAL_PORT_OFFSET..INPCB_LOCAL_PORT_OFFSET + 2]
+                .copy_from_slice(&port.to_be_bytes());
+            internet_record[INPCB_GENERATION_OFFSET..INPCB_GENERATION_OFFSET + 8]
+                .copy_from_slice(&generation.to_ne_bytes());
+            internet_record[INPCB_VERSION_FLAGS_OFFSET] = version_flags;
             match address {
                 IpFixture::V4(address) => {
-                    internet_record[76..80].copy_from_slice(&address.octets());
+                    internet_record[INPCB_IPV4_ADDRESS_OFFSET..INPCB_IPV4_ADDRESS_OFFSET + 4]
+                        .copy_from_slice(&address.octets());
                 }
                 IpFixture::V6(address) => {
-                    internet_record[64..80].copy_from_slice(&address.octets());
+                    internet_record[INPCB_LOCAL_ADDRESS_OFFSET
+                        ..INPCB_LOCAL_ADDRESS_OFFSET + INPCB_LOCAL_ADDRESS_LENGTH]
+                        .copy_from_slice(&address.octets());
                 }
             }
     fn envelope(count: u32, generation: u64, socket_generation: u64) -> Vec<u8> {
-        let mut bytes = Vec::with_capacity(24);
-        bytes.extend(24_u32.to_ne_bytes());
+        let mut bytes = Vec::with_capacity(XINPGEN_LENGTH);
+        bytes.extend((XINPGEN_LENGTH as u32).to_ne_bytes());

The same applies to the - 24 trailer offset at Line 517.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/platform/src/listener/macos/kernel_table.rs` around lines 637 - 703,
Update the fixture builder methods push_listener, push_internet_pcb, and
envelope to derive all record field ranges and envelope/trailer lengths from the
parser’s existing offset and size constants instead of literal offsets such as
18..20, 28..36, 44, 64..80, 76..80, 36..40, and 24. Also replace the trailer’s
“- 24” offset with the corresponding existing constant, keeping fixture layout
synchronized with the parser definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/platform/src/listener/macos/kernel_table.rs`:
- Around line 586-612: Mark
live_kernel_table_repeatedly_detects_all_controlled_listener_classes as an
acceptance-only test using the repository’s existing acceptance-test gating
mechanism, so it is excluded from the normal macOS test pass while remaining
runnable explicitly in acceptance coverage.

---

Nitpick comments:
In `@crates/platform/src/listener/macos.rs`:
- Around line 24-31: Update loopback_tcp_listener_ports to invoke
netstat_tcp_socket_table and parse its results only when
loopback_tcp_listener_ports_from_socket_table returns no ports. Preserve the
existing error propagation and merge behavior for the fallback so missed
listeners remain covered, while avoiding the subprocess on the common non-empty
path.

In `@crates/platform/src/listener/macos/kernel_table.rs`:
- Around line 637-703: Update the fixture builder methods push_listener,
push_internet_pcb, and envelope to derive all record field ranges and
envelope/trailer lengths from the parser’s existing offset and size constants
instead of literal offsets such as 18..20, 28..36, 44, 64..80, 76..80, 36..40,
and 24. Also replace the trailer’s “- 24” offset with the corresponding existing
constant, keeping fixture layout synchronized with the parser definitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4047e29f-54c0-4362-a129-eb65e9c6bb0a

📥 Commits

Reviewing files that changed from the base of the PR and between bd272ce and 99e362b.

⛔ Files ignored due to path filters (5)
  • Cargo.lock is excluded by !**/*.lock
  • crates/platform/src/listener/macos/snapshots/platform__listener__implementation__kernel_table__tests__empty_pcb_fixture_matches_xnu_single_envelope_shape.snap is excluded by !**/*.snap
  • crates/platform/src/listener/macos/snapshots/platform__listener__implementation__kernel_table__tests__live_kernel_table_repeatedly_detects_all_controlled_listener_classes.snap is excluded by !**/*.snap
  • crates/platform/src/listener/macos/snapshots/platform__listener__implementation__kernel_table__tests__malformed_pcb_fixtures_return_deterministic_typed_errors.snap is excluded by !**/*.snap
  • crates/platform/src/listener/macos/snapshots/platform__listener__implementation__kernel_table__tests__pcb_fixture_covers_address_families_states_generations_and_unknown_records.snap is excluded by !**/*.snap
📒 Files selected for processing (9)
  • Cargo.toml
  • crates/platform/Cargo.toml
  • crates/platform/src/lib.rs
  • crates/platform/src/listener.rs
  • crates/platform/src/listener/linux.rs
  • crates/platform/src/listener/macos.rs
  • crates/platform/src/listener/macos/kernel_table.rs
  • crates/platform/src/listener/unsupported.rs
  • crates/platform/src/listener/windows.rs

Comment thread crates/platform/src/listener/macos/kernel_table.rs

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes — The latest commit keeps live macOS kernel-table acceptance coverage out of normal test runs while preserving explicit execution.

  • Gated live listener acceptance coverage — Added #[ignore] to live_kernel_table_repeatedly_detects_all_controlled_listener_classes so routine macOS test runs no longer depend on the host's live TCP PCB table.

Pullfrog  | View workflow run | Using GPT Sol𝕏

@munezaclovis
munezaclovis merged commit 7383e45 into main Jul 28, 2026
6 checks passed
@munezaclovis
munezaclovis deleted the agent/macos-listener-inspector branch July 28, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant