Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions cmd/nuclei/main_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,40 @@ func TestMain(m *testing.M) {
panic(err)
}

dummyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
targetURL = dummyServer.URL
// Only start the shared httptest for benchmarks so unit tests (e.g. goleak)
// are not polluted by a long-lived accept loop.
var dummyServer *httptest.Server
if isBenchmarkRun() {
dummyServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
targetURL = dummyServer.URL
}

// Execute tests

exitCode := m.Run()

// Tear down

dummyServer.Close()
if dummyServer != nil {
dummyServer.Close()
}
_ = os.RemoveAll(projectPath)
_ = os.Unsetenv("DISABLE_STDOUT")

os.Exit(exitCode)
}

func isBenchmarkRun() bool {
for _, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "-test.bench") {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match only the benchmark-selection flag.

This also matches -test.benchmem and -test.benchtime, so unit runs using either flag still start the shared server. Restrict the match to -test.bench or -test.bench=....

Proposed fix
-		if strings.HasPrefix(arg, "-test.bench") {
+		if arg == "-test.bench" || strings.HasPrefix(arg, "-test.bench=") {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if strings.HasPrefix(arg, "-test.bench") {
if arg == "-test.bench" || strings.HasPrefix(arg, "-test.bench=") {
🤖 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 `@cmd/nuclei/main_benchmark_test.go` at line 64, Update the argument check
around the benchmark-detection logic to match only the exact -test.bench flag or
arguments beginning with -test.bench=. Do not treat -test.benchmem or
-test.benchtime as benchmark-selection flags, so they do not start the shared
server.

return true
}
}
return false
}

// getUniqFilename generates a unique filename by appending .N if file exists
// Similar to wget's behavior: file.cpu.prof, file.cpu.1.prof, file.cpu.2.prof, etc.
func getUniqFilename(basePath string) string {
Expand Down
90 changes: 90 additions & 0 deletions cmd/nuclei/main_leak_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main_test

import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"time"

"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/levels"
"github.com/projectdiscovery/nuclei/v3/internal/runner"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/rs/xid"
"github.com/stretchr/testify/require"
"github.com/tarunKoyalwar/goleak"
)

const cliLeakTestChildEnv = "NUCLEI_CLI_LEAKTEST_CHILD"

var cliKnownLeaks = []goleak.Option{
goleak.Pretty(),
goleak.IgnoreAnyFunction("net/http.(*http2ClientConn).readLoop"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).readLoop"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).writeLoop"),
goleak.IgnoreAnyContainingPkg("github.com/hashicorp/golang-lru/v2/expirable"),
goleak.IgnoreAnyContainingPkg("github.com/syndtr/goleveldb"),
goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/ratelimit"),
goleak.IgnoreAnyFunction("github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate.StartActiveMemGuardian.func1"),
}

// TestCLIRunnerGoroutineLeak runs a minimal CLI-path nuclei scan via internal/runner
// and asserts no unexpected goroutine leaks remain after Close.
//
// The body runs in a child process so shared package state from other tests
// does not pollute goleak (same approach as lib/tests SDK leak coverage).
func TestCLIRunnerGoroutineLeak(t *testing.T) {
if os.Getenv(cliLeakTestChildEnv) == "1" {
runCLIRunnerLeakTest(t)
return
}

cmd := exec.Command(os.Args[0], "-test.run=^TestCLIRunnerGoroutineLeak$", "-test.count=1", "-test.v")
cmd.Env = append(os.Environ(), cliLeakTestChildEnv+"=1")
out, err := cmd.CombinedOutput()
require.NoError(t, err, "CLI leak test child failed:\n%s", string(out))
}

func runCLIRunnerLeakTest(t *testing.T) {
gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent)
_ = os.Setenv("DISABLE_STDOUT", "true")
t.Cleanup(func() { _ = os.Unsetenv("DISABLE_STDOUT") })
config.DefaultConfig.DisableUpdateCheck()

defer func() {
time.Sleep(2 * time.Second)
goleak.VerifyNone(t, cliKnownLeaks...)
}()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()

_, thisFile, _, ok := runtime.Caller(0)
require.True(t, ok)
templatePath := filepath.Join(filepath.Dir(thisFile), "testdata", "leaktest", "basic-http.yaml")

options := getDefaultOptions()
options.Targets = []string{server.URL}
options.Templates = []string{templatePath}
options.NoInteractsh = true
options.DisableStdin = true
options.BulkSize = 1
options.TemplateThreads = 1
options.PayloadConcurrency = 1
options.ExecutionId = xid.New().String()

runner.ParseOptions(options)

nucleiRunner, err := runner.New(options)
require.NoError(t, err)
require.NotNil(t, nucleiRunner)
defer nucleiRunner.Close()

require.NoError(t, nucleiRunner.RunEnumeration())
}
16 changes: 16 additions & 0 deletions cmd/nuclei/testdata/leaktest/basic-http.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
id: leaktest-basic-http

info:
name: CLI Leak Test Basic HTTP
author: pdteam
severity: info

http:
- method: GET
path:
- "{{BaseURL}}/"

matchers:
- type: status
status:
- 204
Loading