-
Notifications
You must be signed in to change notification settings - Fork 1
feat(ping): implement ping builtin #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4ddbe7f
4a44531
e90fff2
4f909a7
e9d5931
8afa782
0d5ea98
12e94b5
7242f5e
23e7908
611a19d
0607659
1ffdb32
07b71da
99399d7
ac7eda3
478add3
826b09e
d3b1b4c
55323bd
c2ecac6
2397dc4
4210d91
086baf3
16f17d9
4662dea
cbbd270
0b81c2f
cafd2e6
3881174
b4cb062
05321fb
bc6cb76
10b3aa8
1b3c8bd
831479f
05d7863
193c847
cf3ffa2
232c4d3
838b326
a753d28
5266018
9787106
d50a6cb
a780cb4
861a558
0aa523c
daf4eb7
f02da46
b441173
e972d7a
bf68474
34e6c7b
6201a2d
b6effdb
70d883b
1db555b
d642114
f82a149
cb1343d
3e7a22c
70963f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,13 @@ jobs: | |
| - pkg: ./builtins/tests/ss/ | ||
| name: ss | ||
| corpus_path: builtins/tests/ss | ||
| - pkg: ./builtins/ping/ | ||
| name: ping | ||
| # ping fuzz tests live in builtins/ping/ rather than builtins/tests/ping/ | ||
| # because they share test helper functions (runScriptCtx, etc.) defined in | ||
| # ping_test.go. Go test helpers are only in scope within the same directory, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
All other fuzz targets live under The inline comment in the diff explains why — shared test helper functions ( This is noted for awareness only; no action required if the co-location is intentional. |
||
| # so both files must reside in builtins/ping/. | ||
| corpus_path: builtins/ping | ||
|
AlexandreYang marked this conversation as resolved.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Every other builtin in this matrix uses This is functionally correct (the cache will store/restore Consider either:
This does not block merge. |
||
| - pkg: ./interp/tests/ | ||
| name: interp | ||
| corpus_path: interp/tests | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| // Unless explicitly stated otherwise all files in this repository are licensed | ||
| // under the Apache License Version 2.0. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
| // Copyright 2026-present Datadog, Inc. | ||
|
|
||
| // Security-focused (pentest) tests for the ping builtin. | ||
| // Each test exercises a class of attack and verifies the implementation | ||
| // behaves safely (no crash, no hang, exit 0 or 1 only). | ||
|
|
||
| package ping_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // ============================================================================ | ||
| // GTFOBins / dangerous flag vectors | ||
| // ============================================================================ | ||
|
|
||
| func TestPingPentestFloodRejected(t *testing.T) { | ||
| // -f flood is a DoS vector against the target network. | ||
| _, stderr, code := cmdRun(t, "ping -f 127.0.0.1") | ||
| assert.Equal(t, 1, code, "-f must be rejected as unknown flag") | ||
| assert.Contains(t, stderr, "ping:") | ||
| } | ||
|
|
||
| func TestPingPentestBroadcastRejected(t *testing.T) { | ||
| // -b allows pinging broadcast addresses, which can amplify traffic. | ||
| _, stderr, code := cmdRun(t, "ping -b 255.255.255.255") | ||
| assert.Equal(t, 1, code, "-b must be rejected as unknown flag") | ||
| assert.Contains(t, stderr, "ping:") | ||
| } | ||
|
|
||
| func TestPingPentestSizeRejected(t *testing.T) { | ||
| // -s would let the caller control payload size; not implemented. | ||
| _, stderr, code := cmdRun(t, "ping -s 65507 localhost") | ||
| assert.Equal(t, 1, code) | ||
| assert.Contains(t, stderr, "ping:") | ||
| } | ||
|
|
||
| func TestPingPentestInterfaceRejected(t *testing.T) { | ||
| // -I interface binding not implemented. | ||
| _, stderr, code := cmdRun(t, "ping -I eth0 localhost") | ||
| assert.Equal(t, 1, code) | ||
| assert.Contains(t, stderr, "ping:") | ||
| } | ||
|
|
||
| func TestPingPentestPatternRejected(t *testing.T) { | ||
| // -p pattern filling not implemented. | ||
| _, stderr, code := cmdRun(t, "ping -p ff localhost") | ||
| assert.Equal(t, 1, code) | ||
| assert.Contains(t, stderr, "ping:") | ||
| } | ||
|
|
||
| func TestPingPentestRecordRouteRejected(t *testing.T) { | ||
| // -R record-route not implemented. | ||
| _, stderr, code := cmdRun(t, "ping -R localhost") | ||
| assert.Equal(t, 1, code) | ||
| assert.Contains(t, stderr, "ping:") | ||
| } | ||
|
|
||
| // ============================================================================ | ||
| // Integer overflow / boundary inputs | ||
| // ============================================================================ | ||
|
|
||
| func TestPingPentestCountNegative(t *testing.T) { | ||
| // Negative count clamped to 1; should not hang. | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| // DNS resolution of "no-such-host-xyzzy.invalid" should fail quickly. | ||
| _, _, code := runScriptCtx(ctx, t, "ping -c -99 no-such-host-xyzzy.invalid") | ||
| assert.Equal(t, 1, code) | ||
| assert.NoError(t, ctx.Err(), "should not exceed 5-second deadline") | ||
| } | ||
|
|
||
| func TestPingPentestCountZero(t *testing.T) { | ||
| // -c 0 is clamped to 1; should not hang. | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| _, _, _ = runScriptCtx(ctx, t, "ping -c 0 no-such-host-xyzzy.invalid") | ||
| assert.NoError(t, ctx.Err(), "clamped count=0 should not hang") | ||
| } | ||
|
|
||
| func TestPingPentestCountOverflow(t *testing.T) { | ||
| // Very large count clamped to 20. | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| _, _, _ = runScriptCtx(ctx, t, "ping -c 2147483647 no-such-host-xyzzy.invalid") | ||
| assert.NoError(t, ctx.Err(), "large count should not hang (clamped + DNS fails fast)") | ||
| } | ||
|
|
||
| func TestPingPentestIntervalTooShort(t *testing.T) { | ||
| // Interval below 200ms floor; should not hang. | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| _, _, _ = runScriptCtx(ctx, t, "ping -c 1 -i 1ns no-such-host-xyzzy.invalid") | ||
| assert.NoError(t, ctx.Err()) | ||
| } | ||
|
|
||
| // ============================================================================ | ||
| // Path / host injection | ||
| // ============================================================================ | ||
|
|
||
| func TestPingPentestShellInjectionInHost(t *testing.T) { | ||
| // Verify that the literal string "$(id)" is treated as a hostname (DNS | ||
| // lookup fails) rather than triggering command substitution inside the | ||
| // builtin. The argument is single-quoted so the shell passes it verbatim | ||
| // to ping; only the resolver sees it. | ||
| _, _, code := cmdRun(t, `ping -c 1 '$(id)'`) | ||
| assert.Equal(t, 1, code) | ||
| } | ||
|
|
||
| func TestPingPentestLongHostname(t *testing.T) { | ||
| // A very long hostname should result in a DNS error, not a crash. | ||
| host := strings.Repeat("a", 10000) + ".invalid" | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| _, _, code := runScriptCtx(ctx, t, "ping -c 1 -- "+host) | ||
| assert.Equal(t, 1, code, "long hostname should fail with exit 1") | ||
| assert.NoError(t, ctx.Err(), "should not hang on long hostname") | ||
| } | ||
|
|
||
| func TestPingPentestEmptyHostname(t *testing.T) { | ||
| // Empty hostname (after shell expansion) should give a clear error. | ||
| _, stderr, code := cmdRun(t, `ping -c 1 ""`) | ||
| assert.Equal(t, 1, code) | ||
| assert.Contains(t, stderr, "ping:") | ||
| } | ||
|
|
||
| // ============================================================================ | ||
| // Context cancellation (timeout safety) | ||
| // ============================================================================ | ||
|
|
||
| func TestPingPentestHeaderPrintedBeforeICMP(t *testing.T) { | ||
| // The PING header is emitted after DNS resolution but before opening the | ||
| // ICMP socket. This test verifies the invariant: stdout contains the | ||
| // header even when the ICMP step fails (e.g. EPERM or timeout). | ||
| // 192.0.2.1 is an RFC 5737 documentation address that never responds. | ||
| stdout, _, code := cmdRun(t, "ping -c 1 -W 100ms 192.0.2.1") | ||
| assert.Equal(t, 1, code) | ||
| assert.Contains(t, stdout, "PING 192.0.2.1 (192.0.2.1):", "PING header must appear in stdout even when ICMP fails") | ||
| } | ||
|
|
||
| func TestPingPentestContextTimeout(t *testing.T) { | ||
| // With a very short outer deadline, RunWithContext must exit promptly. | ||
| ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) | ||
| defer cancel() | ||
| start := time.Now() | ||
| _, _, _ = runScriptCtx(ctx, t, "ping -c 10 -W 10s -i 5s 192.0.2.1") | ||
| elapsed := time.Since(start) | ||
| assert.Less(t, elapsed, 2*time.Second, "ping must stop when context is cancelled") | ||
| } | ||
|
|
||
| func TestPingPentestHardTotalTimeout(t *testing.T) { | ||
| // The builtin computes a hard total timeout. With maxed-out count and | ||
| // wait, the deadline must still be within the 120s cap. | ||
| // We use an unresolvable host so execution terminates on DNS failure. | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| // -c 20 -W 30s -i 60s = 20*(60+30)+5 = 1805s → capped at 120s. But DNS | ||
| // failure returns immediately. | ||
| _, _, code := runScriptCtx(ctx, t, "ping -c 20 -W 30s -i 60s no-such-host-xyzzy.invalid") | ||
| assert.Equal(t, 1, code) | ||
| assert.NoError(t, ctx.Err(), "should not exceed 5-second outer deadline") | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.