feat(ip): add ip route show/get builtin (Linux only)#135
feat(ip): add ip route show/get builtin (Linux only)#135AlexandreYang merged 87 commits intomainfrom
Conversation
Adds `ip route [show|list]` and `ip route get ADDRESS` subcommands to the existing `ip` builtin. Routes are read from /proc/net/route via callCtx.OpenFile (respecting the AllowedPaths sandbox); all write operations (add, del, flush, replace, change, save, restore) are blocked with exit 1. Key implementation details: - Little-endian uint32 hex decoding for /proc/net/route IP fields - Longest-prefix-match lookup for `ip route get` - Memory safety: MaxLineBytes (1 MiB) cap per line, maxRoutes (10 000) cap - Linux-only guard (runtime.GOOS); other platforms return a clear error - ProcNetRoutePath exported for testability (tests override with temp file) Also adds: - Linux-only Go tests (ip_linux_test.go) with synthetic /proc/net/route files - Pentest tests (ip_pentest_linux_test.go) for adversarial inputs - Fuzz tests (ip_route_fuzz_linux_test.go): FuzzIPRouteParse, FuzzIPRouteGetAddr - YAML scenario tests for blocked write ops, error cases, and IPv6 rejection - SHELL_FEATURES.md updated to document ip route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9df211bc3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ip route command- FuzzIPRouteGetAddr: add '~' to shell metacharacter filter. Tilde
expansion is blocked by the safe shell (returns exit code 2 via
validateNode), but '~' was missing from the filter list.
- TestIPPentestRouteSubcmd: skip on Linux since ip route is now
implemented there. The test was written when ip route was not yet
implemented; it now skips on Linux where ip route show exits 0.
- FuzzTailLinesOffset, FuzzWcDifferential{Lines,Words,Bytes}: change
per-iteration context from context.Background() to t.Context() so
the fuzz engine's cancellation (at fuzztime=30s) propagates into
running iterations. Without this, workers ignore the fuzz engine's
stop signal and the engine reports "context deadline exceeded".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tween integer types Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@codex review |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@codex review |
…l errors FuzzIPRouteGetAddr and FuzzIPRouteParse used runScriptCtx which calls t.Fatalf on any non-ExitStatus interpreter error. A cached corpus entry triggered an "internal error" from the shell interpreter, failing CI. Switch both to cmdRunCtxFuzz (already used by FuzzIPSubcommand/FuzzIPFlags) which returns -1 for internal errors so the fuzz body can skip them cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 24aa04a7a0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
Mirror the existing ip route get validation: any extra tokens after "show" or "list" now return exit 1 with an "unsupported argument" error instead of silently producing the full routing table. Added TestIPRouteShowTrailingArgRejected and TestIPRouteListTrailingArgRejected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code Review —
|
| Code path | Scenario test | Go test | Status |
|---|---|---|---|
ip route show — default route |
tests/scenarios/cmd/ip/ |
ip_linux_test.go |
Covered |
ip route show — empty file |
— | ip_pentest_linux_test.go:82 |
Covered |
ip route show — /dev/zero hang guard |
skip (intentional) | ip_pentest_linux_test.go:99 |
Covered |
ip route show — long lines |
— | ip_pentest_linux_test.go:127 |
Covered |
ip route show — metric overflow |
— | ip_pentest_linux_test.go:218 |
Covered |
ip route show — binary content |
— | ip_pentest_linux_test.go:244 |
Covered |
ip route get <addr> — valid address |
— | ip_pentest_linux_test.go:287 |
Go only (missing scenario) |
ip route get <addr> — invalid address |
— | ip_pentest_linux_test.go:37 |
Covered |
ip route — unknown subcommand |
tests/scenarios/cmd/ip/errors/ |
— | Covered (weak assertion) |
LongestPrefixMatch — metric tie-breaking |
— | procnet_linux_test.go |
Covered |
Fuzz: arbitrary /proc/net/route content |
— | ip_route_fuzz_linux_test.go:58 |
Covered |
Fuzz: arbitrary ip route get address |
— | ip_route_fuzz_linux_test.go:159 |
Covered (data race) |
Positive Observations
MaxLineBytescap on thebufio.Scannercorrectly protects against/dev/zero-style infinite reads — the pentest test confirms this.parseIPv4validates all four octets fit in[0,255]before assembling theuint32, preventing integer overflow.LongestPrefixMatchmetric tie-breaking (r.Metric < best.Metric) is correct — lower metric wins, matching Linux routing table semantics.parseRouteEntryvalidates field count before any indexing, preventing out-of-bounds panics on malformed lines.- The
procnetinternal package cleanly separates/proc/net/routeparsing from theipcommand logic, andProcNetRoutePathmakes the path injectable for tests. - The fuzz seed corpus is thorough: it covers all CVE-class inputs (null bytes, CRLF, binary magic, long lines) and all existing test inputs as required by the seed corpus comment header.
🤖 Generated with Claude Code
|
Found 1 review item.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c91e7ee9b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Add sync.Mutex around ProcNetRoutePath in writeFuzzRoute to prevent data race between parallel fuzz workers (P1) - Add explanatory comment on os.Open in procnet_linux.go documenting the intentional sandbox bypass for /proc/net/route (P2) - Strengthen unknown_object.yaml assertion from stderr_contains to exact stderr match (P3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@codex review |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Fixed in ea57fc9: updated |
AlexandreYang
left a comment
There was a problem hiding this comment.
Review Summary
Reviewed feat(ip): add ip route show/get builtin (branch alex/ip_route vs main).
This PR adds ip route [show|list] and ip route get ADDRESS subcommands backed by a new builtins/internal/procnet package that reads /proc/net/route directly. All write subcommands are properly blocked. The implementation is thorough, with functional tests, pentest tests, and fuzz tests.
Overall assessment: safe to merge. No P0/P1 issues found. Three minor P3 observations below.
Positive Observations
- Resource exhaustion protection:
MaxRoutes(10 000) andMaxLineBytes(1 MiB) caps prevent unbounded allocation;/dev/zerois covered by a pentest test. - Context cancellation: checked at every iteration in both the scanner loop (
readRoutes) and the output loop (routeShow). /procaccess documented:os.Openbypass of the AllowedPaths sandbox is explicitly justified (matchesssprecedent) and noted in the package-level doc comment.- Write ops comprehensively blocked: all of
add,del,delete,change,replace,flush,save,restorereturn exit 1; each has its own scenario test. - Safe path construction:
filepath.Join(procPath, "net", "route")— no string concatenation. parseIPv4validation: rejects non-decimal, overflow octets, too few/many parts, and IPv6 addresses cleanly.- Correct LPM tie-breaking: lower metric wins on equal prefix length; covered by
TestIPRouteGetMetricTieBreak. formatRoutedefault-route detection: onlyDest==0 && Mask==0prints"default"; theTestIPRouteShowZeroDestNonZeroMaskNotDefaulttest pins this.- Fuzz corpus: seeds cover implementation boundaries, CVE-class inputs (null bytes, CRLF, binary magic, near-MaxLineBytes), and all existing test variants.
…scenario test - Fix error message colon inconsistency in routeCmd show/list branch: "ip: route show: unsupported argument" → "ip: route: show: unsupported argument" to match the consistent "ip: route: <sub>: <msg>" pattern used elsewhere. - Remove dead `strings.ToLower` entry from ip per-command allowed-symbols allowlist. The subcommand dispatch is now case-sensitive (direct string switch); ToLower is no longer called in ip.go. - Fix misleading comment in procnetroute.ReadRoutes that conflated two different invariants into one "No runtime assertion enforces this" statement. Invariant (a) /proc-prefix is caller-enforced only; invariant (b) dotdot check IS enforced at runtime. Document them separately and note the deliberately conservative substring-match behaviour. - Add comment in formatRoute noting the "unreachable default" arm (FlagReject + Dest==0 && Mask==0) is theoretically handled but unlikely to be generated by a real Linux kernel. - Add scenario test route_token_as_subcmd.yaml documenting that a non-subcommand token (e.g. "from") passed as the first argument to "ip route" is treated as an unknown subcommand rather than an unsupported argument to "show". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AlexandreYang
left a comment
There was a problem hiding this comment.
Review: feat(ip): add ip route show/get builtin (Linux only)
Overall Assessment: safe to merge (no P0/P1/P2 findings)
This PR adds ip route [show|list] and ip route get ADDRESS to the existing ip builtin. The implementation is well-structured and security-conscious. I reviewed:
builtins/ip/ip.go— command dispatch, subcommand parsing, output formattingbuiltins/internal/procnetroute/—/proc/net/routeparsing, LPM, path validationallowedsymbols/symbols_builtins.go+symbols_internal.go— symbol allowlist additions- All test files:
ip_linux_test.go,ip_pentest_linux_test.go,ip_route_fuzz_linux_test.go - All YAML scenario tests in
tests/scenarios/cmd/ip/ SHELL_FEATURES.mdandREADME.mddocumentation updates
Security Assessment
Sandbox integrity: os.Open is called directly in procnetroute.readRoutes, bypassing AllowedPaths. This is intentional and correctly documented in AGENTS.md, the package doc comment, and README.md. The path is always derived from ProcNetRoutePath which is hardcoded to /proc in production and never controlled by user input. The .. component check before filepath.Clean correctly blocks traversal sequences.
No user input reaches os.Open. ip route get ADDR passes user input only through parseIPv4 which converts it to a uint32; the result never touches the filesystem path.
Write operations: all of add, del, delete, change, replace, flush, save, restore are blocked with exit 1. -6 (IPv6), --oneline, --brief are rejected for route. ip netns, -b/-B/-batch, --force, -n/--netns are already blocked from the pre-existing implementation.
Memory safety: MaxRoutes (10 000) caps entries in memory; MaxTotalLines (100 000) caps scan time; MaxLineBytes (1 MiB) per line caps allocations. All caps produce hard errors rather than silent truncation, so callers know when LPM results may be unreliable.
DoS prevention: all read loops check ctx.Err() at each iteration; /dev/zero protection verified in TestIPRoutePentestDevZero.
Concurrency: ProcNetRoutePath is read-only in production; test-only mutations are serialised with procNetRouteMu. Fuzz goroutines also hold this mutex for the duration of each iteration.
Bash Compatibility
ip route is a Linux-only command reading /proc/net/route. The output format (no proto/scope/src fields — not available from /proc/net/route alone) is documented. All scenario tests that intentionally diverge from bash are correctly marked skip_assert_against_bash: true.
Test Coverage
| Code path | Scenario test | Go test | Status |
|---|---|---|---|
ip route show happy path |
— (Linux only, per AGENTS.md) | ip_linux_test.go |
Covered |
ip route list alias |
— | TestIPRouteListAliasForShow |
Covered |
ip route (bare, no subcommand) |
— | TestIPRouteShowDefaultRouteAlias |
Covered |
ip route get ADDRESS happy path |
— | TestIPRouteGetDefaultRoute, TestIPRouteGetNetworkRoute |
Covered |
| LPM tie-break by metric | — | TestIPRouteGetMetricTieBreak |
Covered |
| RTF_REJECT routes in show/get | — | TestIPRouteShowRejectRoute, TestIPRouteGetRejectRouteUnreachable |
Covered |
Host route /32 (no /32 suffix) |
— | TestIPRouteShowHostRouteNoSlash32 |
Covered |
| Non-octet CIDR masks (/25, /17, /28) | — | TestIPRouteShowNonOctetMasks |
Covered |
MaxRoutes cap |
— | TestIPRouteMaxRoutesCap |
Covered |
MaxLineBytes long-line protection |
— | TestIPRoutePentestLongLines |
Covered |
/dev/zero hang protection |
— | TestIPRoutePentestDevZero |
Covered |
| Binary/adversarial file content | — | TestIPRoutePentestBinaryContent |
Covered |
| Write ops blocked (add/del/etc.) | route_blocked/*.yaml |
TestIPRouteAddBlocked, etc. |
Covered |
| Case-sensitive subcommands | route_case_insensitive/show_uppercase.yaml |
TestIPRouteSubcmdCaseSensitive |
Covered |
-6 route blocked |
errors/ipv6_route_not_supported.yaml |
TestIPIPv6RouteBlocked |
Covered |
--oneline/--brief route blocked |
errors/route_brief_not_supported.yaml, route_oneline_not_supported.yaml |
— | Covered |
ip route get missing addr |
errors/route_get_missing_addr.yaml |
TestIPRouteGetMissingAddr |
Covered |
ip route get leading-zero octet |
errors/route_get_leading_zero_octet.yaml |
— | Covered |
| Fuzz: parser + addr parser | — | FuzzIPRouteParse, FuzzIPRouteGetAddr |
Covered |
Coverage: Adequate. All new code paths are covered by Linux Go tests. Scenario tests cover all error/blocked paths that are portable.
Findings
| # | Priority | File | Finding |
|---|---|---|---|
| 1 | tests/scenarios/cmd/ip/route_case_insensitive/show_uppercase.yaml |
Filename says "show_uppercase" but scenario tests "GET" uppercase — minor naming inconsistency |
Positive Observations
- The
..component check is applied to the original path beforefilepath.Clean, which correctly catches traversal sequences like/proc/../etc/passwdthatCleanwould hide. This is a well-known footgun and it's handled correctly. - Returning
ErrMaxRoutes/ErrMaxTotalLinesas hard errors rather than silently truncating is the right design: it prevents LPM from operating on incomplete data and producing a wrong next-hop. - The
IsContiguousMaskfunction correctly handles the little-endian /proc byte order before validating CIDR contiguity — a subtle but important detail. - Fuzz seed corpus covers CVE-class inputs (ELF/PE/ZIP magic bytes, null bytes, CRLF, very long lines) in addition to the normal test cases.
AGENTS.mdwas updated with the AllowedPaths bypass rationale, providing a permanent audit trail for the security trade-off.
|
@codex review this PR |
|
Iteration 2 self-review result: 1 finding (P3/minor). Summary: scenario test file show_uppercase.yaml tests ip route GET (not SHOW) — minor naming inconsistency. No P0/P1/P2 findings. |
…st content The scenario tests `ip route GET` (uppercase GET), not `ip route SHOW`. The old filename `show_uppercase.yaml` was misleading; renaming to `get_uppercase.yaml` makes the filename consistent with the actual subcommand being tested. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AlexandreYang
left a comment
There was a problem hiding this comment.
Review Summary
This PR adds ip route [show|list] and ip route get ADDRESS subcommands to the existing ip builtin, reading the IPv4 routing table from /proc/net/route via a new builtins/internal/procnetroute package. It also refactors the ss builtin to delegate /proc/net/ I/O to a new builtins/internal/procnetsocket package, and extracts a shared builtins/internal/procpath constant.
Overall assessment: safe to merge. The implementation is well-structured, security-conscious, and thoroughly tested. No P0 or P1 findings.
Summary Table
Coverage Summary
| Code path | Scenario test | Go test | Status |
|---|---|---|---|
ip route show — default route |
— | ip_linux_test.go:TestIPRouteShowDefault |
Covered |
ip route show — down routes skipped |
— | ip_linux_test.go:TestIPRouteShowDownRouteSkipped |
Covered |
ip route show — reject (unreachable) route |
— | ip_linux_test.go:TestIPRouteShowRejectRoute |
Covered |
ip route show — non-octet CIDR masks (/25, /17, /20, /28) |
— | ip_linux_test.go:TestIPRouteShowNonOctetMasks |
Covered |
ip route show — host route /32 no "/32" suffix |
— | ip_linux_test.go:TestIPRouteShowHostRouteNoSlash32 |
Covered |
ip route get — longest-prefix-match |
— | ip_linux_test.go:TestIPRouteGetLongestPrefixMatch |
Covered |
ip route get — metric tie-break |
— | ip_linux_test.go:TestIPRouteGetMetricTieBreak |
Covered |
ip route get — reject route returns unreachable |
— | ip_linux_test.go:TestIPRouteGetRejectRouteUnreachable |
Covered |
| Write ops blocked | route_blocked/*.yaml |
ip_linux_test.go:TestIPRoute{Add,Del,...}Blocked |
Covered |
ip -6 route rejected |
errors/ipv6_route_not_supported.yaml |
ip_linux_test.go:TestIPIPv6RouteBlocked |
Covered |
ip route SHOW (case-sensitive) |
route_case_insensitive/get_uppercase.yaml |
ip_linux_test.go:TestIPRouteSubcmdCaseSensitive |
Covered |
| MaxRoutes cap | — | ip_linux_test.go:TestIPRouteMaxRoutesCap |
Covered |
| MaxLineBytes cap (long lines) | — | ip_pentest_linux_test.go:TestIPRoutePentestLongLines |
Covered |
/dev/zero no-hang |
— | ip_pentest_linux_test.go:TestIPRoutePentestDevZero |
Covered |
| Fuzz: parser | — | ip_route_fuzz_linux_test.go:FuzzIPRouteParse |
Covered |
| Fuzz: address parser | — | ip_route_fuzz_linux_test.go:FuzzIPRouteGetAddr |
Covered |
| MaxTotalLines cap | — | — | Not directly tested (P3) |
Findings
P3 Findings
Finding 1: .. check documents but does not enforce /proc-prefix invariant
Severity: P3 — Hardening / documentation gap
Location: builtins/internal/procnetroute/procnetroute.go:107 and the safety invariant comment at lines 93–105
Description: The ReadRoutes doc-comment notes:
(a) /proc-prefix requirement: all callers MUST pass a path that starts with /proc. No runtime assertion enforces this — tests override procPath with a temp-directory tree...
The .. substring check (line 107) catches traversal sequences but does not enforce the /proc-prefix invariant it documents. In practice, ProcNetRoutePath is a package-level constant initialized to procpath.Default ("/proc") and only overridden in tests, so there is no real attack surface. However, a future caller who passes an arbitrary non-/proc path would bypass the stated invariant silently. This is P3 because the variable is not accessible from user scripts.
Remediation (optional): Consider adding a debug-mode assertion or a //nolint comment linking to the security design decision in AGENTS.md. No urgent action needed.
Finding 2: Trailing argument handling for ip route show produces a terse error
Severity: P3 — Minor UX divergence
Location: builtins/ip/ip.go:595-597
Description: ip route show table main (or any unsupported trailing token after show/list) produces ip: route: show: unsupported argument "table" and exits 1, which is safe and correct. The real ip command also rejects this with an error. The divergence is acceptable but is not documented in the scenario tests as an intentional divergence (the scenario test route_brief_not_supported.yaml covers brief/oneline but there is no test for unsupported trailing arguments to show that exercises the exact error message in a bash-comparable way).
Remediation: No action required — the AGENTS.md already documents that ip route happy-path scenario tests cannot be added due to the platform-skip limitation. The error path is already tested in ip_linux_test.go:TestIPRouteShowTrailingArgRejected.
Finding 3: Context cancellation tests are timeout-guards, not actual cancellation tests
Severity: P3 — Test quality
Location: builtins/tests/ip/ip_linux_test.go:627-643
Description: TestIPRouteShowContextCancellation and TestIPRouteGetContextCancellation set a 5-second timeout but never cancel the context early. They effectively just verify the operation completes within 5 seconds. A true context cancellation test would cancel the context immediately after starting the command and verify the builtin returns promptly. The TestIPRoutePentestDevZero test provides stronger coverage of the hang-prevention invariant.
Remediation (optional): Consider adding a test that cancels a context mid-execution (e.g. using a large synthetic route file and a goroutine that cancels after a short delay). Not blocking.
Positive Observations
- Strong security posture:
ProcNetRoutePathis a compile-time constant (procpath.Default = "/proc") only overridden in tests. The sandbox bypass is clearly documented inAGENTS.md,SHELL_FEATURES.md, and inline comments. The..path guard inReadRoutesprovides defence-in-depth. - Memory safety: Both
MaxRoutes(10 000) andMaxTotalLines(100 000) caps are well-chosen. TheMaxLineBytes(1 MiB) per-line cap on the scanner prevents OOM from pathologically long lines. The/dev/zerotest verifies the cap terminates correctly. - No user-controlled paths:
ProcNetRoutePathis never derived from shell script input — it flows throughbuiltins/ip/ip.go→procnetroute.ReadRoutes(ctx, ProcNetRoutePath)withProcNetRoutePathbeing the fixed/procconstant in production. - Write operations correctly blocked: All write subcommands (
add,del,delete,change,replace,flush,save,restore) are matched in the first switch inrouteCmdbefore any I/O, and covered by scenario tests withskip_assert_against_bash: true. - Case-sensitive subcommand matching: The PR intentionally makes
ip routesubcommands case-sensitive to match real iproute2 behavior, with both a Go test and a scenario test. - Well-structured refactoring: Moving socket-table parsing from
ss_linux.gointobuiltins/internal/procnetsocketis a clean separation of concerns that also letsip routereuse theprocpathconstant. - Good fuzz coverage:
FuzzIPRouteParseandFuzzIPRouteGetAddrare registered in thefuzz.ymlCI matrix under./builtins/tests/ip/, ensuring they run on PRs. - Reject-route handling: The RTF_REJECT flag handling is correct — reject routes are included in
ip route showwith anunreachableprefix, nodevfield, andip route getreturns exit 1 withnetwork unreachablewhen the best-match route is a reject route rather than silently falling back to a less-specific route.
|
@codex review this PR |
|
Iteration 3 self-review result: 3 findings (all P3/minor). (1) dotdot guard is defence-in-depth only, (2) terse error on unsupported table arg, (3) context cancellation tests are timeout-guards only. No P0/P1/P2 findings. Safe to merge. |
…ests AGENTS.md requires |+ block scalar for input.script, expect.stdout, and expect.stderr values in all scenario tests. Convert stdout: "" to stdout: |+ across all 18 new ip route scenario files (errors/, route_blocked/, route_case_insensitive/) to comply with this project convention. Also convert the flow-style stderr in unknown_object.yaml to |+ block scalar for consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AlexandreYang
left a comment
There was a problem hiding this comment.
Review Summary
Reviewed the ip route show/get builtin and associated refactoring: procnetroute package creation, procpath centralisation, procnetsocket restructure, and ss table-cap hardening. The implementation is solid overall — all write operations are blocked, the /proc/net/route path is hardcoded and never user-derived, input validation is thorough, and the test suite (unit, pentest, fuzz) is comprehensive.
Overall assessment: safe to merge with one minor correctness fix (P2) and two P3 suggestions.
Summary Table
Findings
F1 — Context cancellation in routeShow returns exit 1 (bash divergence)
Severity: P2 — Bash Compatibility
Location: builtins/ip/ip.go:626-631
Description: When the shell execution context is cancelled (timeout) while routeShow is iterating through already-read route entries, it returns Code: 1. Real ip route show exits 0 regardless of how much output was written before interruption. This diverges from bash semantics.
The test TestIPRouteShowContextCancellation asserts code == 0 || code == 1 — a wide assertion that masks the divergence.
// Current (diverges from bash):
if ctx.Err() != nil {
return builtins.Result{Code: 1}
}Note: the ReadRoutes inner loop also checks ctx.Err() and returns an error if cancelled during reading — that correctly becomes exit 1 (I/O incomplete). The divergence is only in the output loop after routes are already loaded.
Remediation: Change Code: 1 to Code: 0 (i.e. return builtins.Result{}) in the output loop's cancellation guard. This matches how runAddr/runLink handle ctx.Err() — they break and return empty Result{}.
F2 — Error message format inconsistency for trailing-arg rejection
Severity: P3 — Cosmetic
Location: builtins/ip/ip.go:596
Description: route show trailing-arg errors use "ip: route: %s: unsupported argument" while route get uses "ip: route get: unsupported argument". Minor inconsistency in the error namespace prefix.
Remediation: Change to "ip: route %s: unsupported argument %q\n", sub, args[1] for a uniform "ip: route show:" / "ip: route list:" / "ip: route get:" prefix pattern.
F3 — ErrMaxTotalLines path untested
Severity: P3 — Test Coverage
Location: builtins/tests/ip/ip_linux_test.go
Description: The existing TestIPRouteMaxRoutesCap uses 15,000 identical UP-route rows, which hits ErrMaxRoutes before ErrMaxTotalLines. The MaxTotalLines scan-time guard (triggered when >100,000 non-UP/malformed lines appear before reaching MaxRoutes UP entries) is untested. A file with >100,000 non-UP rows and zero UP rows would trigger it.
Remediation: Add a test that writes MaxTotalLines + 1 non-UP rows (flags=0x0000) and asserts exit 1 with "MaxTotalLines" in stderr.
Coverage Summary
| Code path | Scenario test | Go test | Status |
|---|---|---|---|
route show / route list happy path |
— (Linux-only, no platform skip in scenario fw) | ip_linux_test.go |
Covered |
route get ADDRESS LPM |
— | TestIPRouteGetLongestPrefixMatch, MetricTieBreak, HostRoute | Covered |
| Write ops blocked | route_blocked/*.yaml |
ip_linux_test.go |
Covered |
| Unknown subcommand | errors/route_unknown_subcmd.yaml |
✓ | Covered |
| IPv6 route rejected | errors/ipv6_route_not_supported.yaml |
✓ | Covered |
| RTF_REJECT route in show | — | TestIPRouteShowRejectRoute | Covered |
| RTF_REJECT route in get → unreachable | — | TestIPRouteGetRejectRouteUnreachable | Covered |
| MaxRoutes cap | — | TestIPRouteMaxRoutesCap | Covered |
| MaxTotalLines cap | — | — | Missing (P3) |
| Non-octet-aligned CIDR masks (/25, /17, etc.) | — | TestIPRouteShowNonOctetMasks | Covered |
/dev/zero hang protection |
— | TestIPRoutePentestDevZero | Covered |
| Fuzz: parser + address | — | FuzzIPRouteParse, FuzzIPRouteGetAddr | Covered |
| Context cancel → exit code | — | TestIPRouteShowContextCancellation (asserts 0 OR 1) | Partially covered (P2, F1) |
Positive Observations
- Sandbox bypass is fully documented at every layer: package doc,
ReadRoutesgodoc,AGENTS.mdSecurity Design Decisions section, and inline comments. Auditable and intentional. - Two-layer path safety:
strings.Contains(procPath, "..")check on the original path prevents traversal beforefilepath.Cleannormalises it. The doc correctly explains why Clean is applied after, not before, the check. - All write operations blocked with precise error messages, covered by dedicated scenario tests (
route_blocked/*.yaml, all withskip_assert_against_bash: true). - Double-bounded resource exhaustion:
MaxRoutes(memory guard) +MaxTotalLines(scan-time guard), both resulting in hard errors — correct choice since silent truncation would produce incorrect LPM results. IsContiguousMaskcorrectness: the little-endian → network-byte-order byte-swap before CIDR validation is correct.TestIPRouteShowNonOctetMasksvalidates /25, /17, /20, /28 edge cases.ProcNetRoutePathconcurrency contract is clearly documented with mutex, warning againstt.Parallel(), and comments explaining the production-immutability invariant.- GTFOBins vectors blocked and tested:
-b/-B/-batch,--force,-n/--netns,ip netnsall exit 1 with clear messages, covered at both unit and integration test levels.
|
@codex review this PR |
|
Iteration 4 self-review result: 3 findings — 1 P2 (routeShow returns Code:1 on context cancel instead of 0), 2 P3 (error message inconsistency, ErrMaxTotalLines lacks dedicated test). Safe to merge. |
… error prefix Two fixes from self-review comments: 1. routeShow: return exit 0 on context cancellation (matching runAddr/runLink pattern), not exit 1. Real ip route show exits 0 when interrupted after partial output. Updated TestIPRouteShowContextCancellation to assert code==0. 2. routeCmd show/list: use "ip: route %s: unsupported argument" (no extra colon before %s) to match the "ip: route get: unsupported argument" prefix pattern. Also switch from args[0] to sub so the lowercased canonical form is used (sub == args[0] in this branch but avoids relying on args[0]). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AlexandreYang
left a comment
There was a problem hiding this comment.
Review Summary
Scope: ip route [show|list] and ip route get ADDRESS subcommands, builtins/internal/procnetroute package, refactored builtins/internal/procnetsocket package, associated tests, and documentation.
Overall assessment: safe to merge. The implementation is carefully designed with multiple defence-in-depth layers: the ..-traversal guard on procPath, a per-line memory cap (MaxLineBytes), a route-table memory cap (MaxRoutes), a CPU-time guard (MaxTotalLines), context-cancellation checks in every loop, and explicit hard-errors (rather than silent truncation) when caps are hit. All write operations are blocked. GTFOBins vectors are rejected. The AllowedPaths sandbox bypass is intentional, correctly constrained, and documented in both the package doc and AGENTS.md.
No P0, P1, or P2 findings. Three P3 observations (minor hardening / clarity) are left as inline comments.
Positive Observations
- Sandbox bypass is constrained and documented.
ReadRoutescallsos.Opendirectly butprocPathis hardcoded to/procin production, the..guard fires on the original (pre-filepath.Clean) path to catch traversal sequences, and the intent is documented in the package doc, inline,AGENTS.md, andSHELL_FEATURES.md. - Hard-error on cap violations. Returning
ErrMaxRoutes/ErrMaxTotalLinesinstead of silently truncating ensures callers (and users) know that LPM results may be incorrect when the route table is abnormally large. - Context cancellation at every loop iteration. Both
readRoutesandrouteShowcheckctx.Err()in their scan loops, preventing hangs on a slow or infinite/proc/net/route. /dev/zeroprotection.MaxLineBytes(1 MiB) causesbufio.ErrTooLongbefore unbounded memory can be allocated, and the pentest test (TestIPRoutePentestDevZero) verifies this.- Comprehensive test coverage. Unit tests, pentest tests, fuzz tests, and YAML scenario tests cover the happy path, all error paths, boundary conditions, adversarial inputs, and context cancellation.
- Write operations all blocked with clear errors.
add,del,delete,change,replace,flush,save,restoreall exit 1 with a descriptive message.
|
@codex review this PR |
|
Iteration 5 self-review result: 3 findings (all P3/low). (1) equal-cost multipath first-match-wins undocumented, (2) fuzz helper lacks clarifying comment, (3) oneline/brief rejection on default subcommand path is uncommented. No P0/P1/P2 findings. Safe to merge. |
…line flag - procnetroute.go: document equal-prefix equal-metric tie-break as first-match-wins (kernel order), making the invariant explicit for future maintainers. - ip_route_fuzz_linux_test.go: clarify that the no-op cleanup returned on WriteFile failure is correct because no mutex was acquired yet. - ip.go: add comment explaining --oneline/--brief check applies regardless of whether subcommand was explicit or defaulted to "show". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review-Fix Loop Summary
Iteration log
Final state
Remaining issuesNone. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 866e5fb50a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| stderr: |+ | ||
| ip: route: GET: unknown subcommand | ||
| exit_code: 1 | ||
| skip_assert_against_bash: true |
There was a problem hiding this comment.
Remove bash-skip from case-sensitive route scenario
This scenario is marked skip_assert_against_bash: true even though its own description says the behavior should match real iproute2 (uppercase GET rejected). Per /workspace/rshell/AGENTS.md, skip_assert_against_bash: true should only be used for intentional bash divergences; keeping the skip here disables the required byte-for-byte bash check and can let future compatibility regressions in ip route subcommand parsing slip through unnoticed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
[Claude Sonnet 4.6] Fixed. The skip was needed because our error message ("ip: route: GET: unknown subcommand", exit 1) diverged from iproute2 ("Command "GET" is unknown, try "ip route help".", exit 255). Rather than keep the skip, I updated the implementation to match iproute2 exactly: unknown route subcommands now emit the iproute2-format error and exit 255. Updated get_uppercase.yaml and route_unknown_subcmd.yaml to use the new expected output, removed both skips, and updated the Go unit tests accordingly. Also updated route_token_as_subcmd.yaml which tests the same code path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
### What does this PR do? See changes: - https://github.com/DataDog/rshell/releases/tag/v0.0.7 - https://github.com/DataDog/rshell/releases/tag/v0.0.8 Besides the minor fixes to improve security and memory usage, we have those changes to rshell commands: - feat(find): implement -execdir command {} ; by @matt-dz in DataDog/rshell#125 - feat(find): implement -exec command {} ; by @matt-dz in DataDog/rshell#133 - feat(ip): add ip route show/get builtin (Linux only) by @AlexandreYang in DataDog/rshell#135 ### Testing ✅ Tested e2e with local agent ### Describe how you validated your changes Test rshell works e2e ### Additional Notes Co-authored-by: alexandre.yang <alexandre.yang@datadoghq.com>
See changes: - https://github.com/DataDog/rshell/releases/tag/v0.0.7 - https://github.com/DataDog/rshell/releases/tag/v0.0.8 Besides the minor fixes to improve security and memory usage, we have those changes to rshell commands: - feat(find): implement -execdir command {} ; by @matt-dz in DataDog/rshell#125 - feat(find): implement -exec command {} ; by @matt-dz in DataDog/rshell#133 - feat(ip): add ip route show/get builtin (Linux only) by @AlexandreYang in DataDog/rshell#135 ✅ Tested e2e with local agent Test rshell works e2e Co-authored-by: alexandre.yang <alexandre.yang@datadoghq.com>
Summary
ip route [show|list]andip route get ADDRESSsubcommands to the existingipbuiltin (Linux only)/proc/net/routeviacallCtx.OpenFile, respecting the AllowedPaths sandboxadd,del,flush,replace,change,save,restore) are blocked with exit 1/proc/net/routeparsing into a dedicatedbuiltins/internal/procnetpackage for reusabilityImplementation details
/proc/net/routeIP fieldsip route getMaxLineBytes(1 MiB) cap per line,maxRoutes(10 000) capruntime.GOOS); other platforms return a clear errorProcNetRoutePathexported for testability (tests override with temp file)Tests
ip_linux_test.go) with synthetic/proc/net/routefilesip_pentest_linux_test.go) for adversarial inputsip_route_fuzz_linux_test.go):FuzzIPRouteParse,FuzzIPRouteGetAddrSHELL_FEATURES.mdupdated to documentip routeGenerated with Claude Code