Skip to content

fix: indefinite hangs during TLS handshake and cipher enumeration#960

Open
allornothingai wants to merge 3 commits intoprojectdiscovery:mainfrom
allornothingai:fix/indefinite-hangs
Open

fix: indefinite hangs during TLS handshake and cipher enumeration#960
allornothingai wants to merge 3 commits intoprojectdiscovery:mainfrom
allornothingai:fix/indefinite-hangs

Conversation

@allornothingai
Copy link

@allornothingai allornothingai commented Mar 15, 2026

This PR fixes multiple indefinite hangs in tlsx:

  1. Fixed a race condition/deadlock in ztls.tlsHandshakeWithTimeout where the select statement would block during function call evaluation, making the context timeout ineffective.
  2. Added missing context timeouts to EnumerateCiphers in both ztls and ctls (stdlib) clients.
  3. Increased the buffer size and implemented non-blocking sends for the PDCP UploadWriter to prevent worker goroutines from stalling on slow cloud uploads.

These fixes resolve issue #819 where tlsx hangs indefinitely after processing a large number of targets.

Summary by CodeRabbit

  • Bug Fixes
    • Increased internal buffering and made data sends non-blocking; dropped sends are counted and logged, with a shutdown warning if drops occurred.
    • Improved TLS operations to use per-call timeouts and cancellable contexts for handshakes, with consolidated error handling and clearer failure reporting.

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Mar 15, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Fixes race condition in ztls TLS handshake timeout implementation by using buffered error channel
  • Adds missing context timeouts to cipher enumeration in both ztls and ctls clients
  • Implements non-blocking sends with increased buffer size for PDCP upload writer to prevent goroutine stalls
Hardening Notes
  • Consider adding rate limiting on PDCP upload operations to prevent resource exhaustion if the dropped counter grows too large
  • The droppedCounter tracking in writer.go:98 could benefit from a maximum threshold alert to notify operators of sustained buffer pressure

Comment @pdneo help for available commands. · Open in Neo

@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

Walkthrough

TLS handshakes were made context-aware with per-call timeouts and refactored to run the handshake in a goroutine with context cancellation; PDCP upload writer buffer was enlarged and channel sends made non-blocking with dropped-result counting and warning logs.

Changes

Cohort / File(s) Summary
PDCP Buffer & UploadWriter
internal/pdcp/writer.go
Increased data channel buffer from 8 to 1000; added droppedCounter to UploadWriter; channel sends in GetWriterCallback are non-blocking and increment the counter + log a warning when the buffer is full; autoCommit logs if results were dropped at termination.
TLS — crypto/tls integration
pkg/tlsx/tls/tls.go
Replaced plain Handshake with HandshakeContext(ctx) using a per-connection context with timeout (from options) and ensured proper cancellation and connection close.
ztls handshake refactor
pkg/tlsx/ztls/ztls.go
Introduced per-call context-with-timeout usage, changed tlsHandshakeWithTimeout signature to (ctx context.Context, tlsConn *tls.Conn) error, refactored handshake to run in a goroutine and select on handshake result vs. context cancellation, consolidated error handling (treat tls.ErrCertsOnly as nil).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ZTLS as ztls.Client
    participant Goroutine as Handshake Goroutine
    participant Server as Remote TLS Server
    participant CtxMgr as Context (with timeout)

    Client->>ZTLS: EnumerateCiphers / ConnectWithOptions
    ZTLS->>CtxMgr: create ctx (with optional timeout)
    ZTLS->>Goroutine: start goroutine to run tlsConn.Handshake()
    ZTLS->>CtxMgr: monitor ctx.Done()
    Goroutine->>Server: perform Handshake (tlsConn.Handshake)
    alt Handshake completes
        Server-->>Goroutine: handshake result/error
        Goroutine-->>ZTLS: send error on errChan
        ZTLS->>ZTLS: receive err, treat tls.ErrCertsOnly as nil
    else Context cancels / times out
        CtxMgr-->>ZTLS: ctx.Done signal
        ZTLS->>Goroutine: consider handshake aborted (select chooses ctx)
    end
    ZTLS->>CtxMgr: cancel context and cleanup
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through handshakes, swift and neat,
Goroutines drummed a steady beat,
Timeouts kept pace, buffers grew wide,
Dropped counts logged when flow couldn't hide,
A little rabbit nods—safe on both sides.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main objective: fixing indefinite hangs during TLS handshake and cipher enumeration, which aligns with all the key changes across the modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
pkg/tlsx/ztls/ztls.go (1)

326-341: Goroutine-based timeout pattern correctly fixes the race condition.

The buffered channel (size 1) ensures the goroutine won't leak when the context times out before the handshake completes. The ErrCertsOnly handling aligns with the client's CertsOnly mode.

Minor convention note: Go idiom places ctx context.Context as the first parameter. Consider reordering for consistency with standard library patterns.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/tlsx/ztls/ztls.go` around lines 326 - 341, The function
tlsHandshakeWithTimeout currently takes (tlsConn *tls.Conn, ctx
context.Context); reorder parameters to place ctx context.Context first to
follow Go conventions (i.e., change signature to tlsHandshakeWithTimeout(ctx
context.Context, tlsConn *tls.Conn)), and update all call sites that invoke
tlsHandshakeWithTimeout to pass the context as the first argument; keep the
existing logic inside the function (channel/goroutine handshake and ErrCertsOnly
handling) unchanged.
internal/pdcp/writer.go (1)

94-98: Non-blocking send prevents hangs but silently drops data.

The select/default pattern correctly prevents goroutine stalls. However, when the buffer is full, results are permanently lost with only a warning log. Consider tracking dropped results with a counter (similar to u.counter for uploads) so users can verify scan completeness at the end.

♻️ Suggested enhancement to track dropped results

Add a field to UploadWriter:

droppedCounter atomic.Int32

Then update the callback:

 func (u *UploadWriter) GetWriterCallback() func(*clients.Response) {
 	return func(resp *clients.Response) {
 		select {
 		case u.data <- resp:
 		default:
+			u.droppedCounter.Add(1)
 			gologger.Warning().Msgf("PDCP upload buffer full, skipping result")
 		}
 	}
 }

And report in autoCommit defer:

if dropped := u.droppedCounter.Load(); dropped > 0 {
    gologger.Warning().Msgf("Dropped %v results due to upload buffer overflow", dropped)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/pdcp/writer.go` around lines 94 - 98, Add a dropped results counter
to UploadWriter and increment it whenever the non-blocking send to u.data fails;
specifically add a droppedCounter (e.g., atomic.Int32) to the UploadWriter
struct, replace the select/default branch that currently logs "PDCP upload
buffer full, skipping result" with logic that increments u.droppedCounter and
still logs the warning, and then in autoCommit's defer read/report the counter
(u.droppedCounter.Load()) and emit a warning if >0 so callers can verify dropped
results (keep existing u.counter for successful uploads).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/tlsx/tls/tls.go`:
- Around line 239-244: The handshake loop uses context.WithTimeout with
c.options.Timeout which if zero yields an already-expired context and makes
conn.HandshakeContext fail; modify the code around
context.WithTimeout/conn.HandshakeContext so it first checks c.options.Timeout
!= 0 and only creates a timeout context in that case, otherwise use
context.Background() (or context.WithCancel) for the handshake; update the
cancel() usage accordingly to only call cancel when a cancellable timeout
context was created (refer to c.options.Timeout, context.WithTimeout,
conn.HandshakeContext, and how ConnectWithOptions guards Timeout).

In `@pkg/tlsx/ztls/ztls.go`:
- Around line 260-265: The cipher-enumeration loop uses context.WithTimeout with
c.options.Timeout which when zero makes the context expire immediately; update
the code around tlsHandshakeWithTimeout and the block that builds ctx, cancel to
guard for c.options.Timeout == 0 (same pattern as ConnectWithOptions) by
choosing a sensible default or skipping the timeout logic so handshakes can run,
then call tlsHandshakeWithTimeout(conn, ctx) and cancel() as before; reference
c.options.Timeout, tlsHandshakeWithTimeout, conn.GetHandshakeLog and
enumeratedCiphers to locate and apply the guard.

---

Nitpick comments:
In `@internal/pdcp/writer.go`:
- Around line 94-98: Add a dropped results counter to UploadWriter and increment
it whenever the non-blocking send to u.data fails; specifically add a
droppedCounter (e.g., atomic.Int32) to the UploadWriter struct, replace the
select/default branch that currently logs "PDCP upload buffer full, skipping
result" with logic that increments u.droppedCounter and still logs the warning,
and then in autoCommit's defer read/report the counter (u.droppedCounter.Load())
and emit a warning if >0 so callers can verify dropped results (keep existing
u.counter for successful uploads).

In `@pkg/tlsx/ztls/ztls.go`:
- Around line 326-341: The function tlsHandshakeWithTimeout currently takes
(tlsConn *tls.Conn, ctx context.Context); reorder parameters to place ctx
context.Context first to follow Go conventions (i.e., change signature to
tlsHandshakeWithTimeout(ctx context.Context, tlsConn *tls.Conn)), and update all
call sites that invoke tlsHandshakeWithTimeout to pass the context as the first
argument; keep the existing logic inside the function (channel/goroutine
handshake and ErrCertsOnly handling) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5846bef1-b0a3-42d1-9379-34888db32676

📥 Commits

Reviewing files that changed from the base of the PR and between d13b67f and 37436e1.

📒 Files selected for processing (3)
  • internal/pdcp/writer.go
  • pkg/tlsx/tls/tls.go
  • pkg/tlsx/ztls/ztls.go

Guard against Timeout == 0 causing immediate context expiration. Co-authored-by: Paperclip <noreply@paperclip.ing>
@allornothingai
Copy link
Author

I have addressed the review comments regarding timeout validation in . Added guards against to prevent immediate context expiration.

- Add dropped results counter to PDCP UploadWriter\n- Reorder parameters of tlsHandshakeWithTimeout to follow Go conventions\n\nCo-authored-by: Paperclip <noreply@paperclip.ing>
@allornothingai
Copy link
Author

I have addressed the review comments and nitpicks:

  • Added timeout validation in EnumerateCiphers to prevent immediate context expiration when Timeout == 0.
  • Added a droppedCounter to UploadWriter to track and report results skipped due to buffer overflow.
  • Reordered tlsHandshakeWithTimeout parameters to place context.Context first, following Go conventions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
pkg/tlsx/ztls/ztls.go (1)

260-264: ⚠️ Potential issue | 🔴 Critical

Set a default 5-second timeout in the ztls cipher enumeration when no timeout is provided.

The codebase convention (as seen in pkg/tlsx/clients/utils.go and documented in cmd/tlsx/main.go) establishes that Timeout == 0 should use a 5-second default. The current code at lines 260-264 breaks this by using context.Background() instead, which creates a context without any deadline. This allows cipher enumeration to hang indefinitely against servers that do not complete the TLS handshake.

Proposed fix
-		ctx := context.Background()
-		var cancel context.CancelFunc
-		if c.options.Timeout != 0 {
-			ctx, cancel = context.WithTimeout(ctx, time.Duration(c.options.Timeout)*time.Second)
-		}
+		timeout := time.Duration(c.options.Timeout) * time.Second
+		if timeout == 0 {
+			timeout = 5 * time.Second
+		}
+		ctx, cancel := context.WithTimeout(context.Background(), timeout)
 		if err := c.tlsHandshakeWithTimeout(ctx, conn); err == nil {
 			h1 := conn.GetHandshakeLog()
 			enumeratedCiphers = append(enumeratedCiphers, h1.ServerHello.CipherSuite.String())
 		}
-		if cancel != nil {
-			cancel()
-		}
+		cancel()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/tlsx/ztls/ztls.go` around lines 260 - 264, The cipher enumeration uses
context.Background() when c.options.Timeout == 0, which allows hangs; change
this so that when c.options.Timeout == 0 you create a context with a 5-second
timeout instead (use context.WithTimeout(ctx, 5*time.Second)) and ensure the
resulting cancel function is assigned and deferred; update the logic around ctx,
cancel in the ztls cipher enumeration code (referencing c.options.Timeout, ctx
and cancel in ztls.go) so the default timeout follows the project convention.
🧹 Nitpick comments (1)
internal/pdcp/writer.go (1)

95-100: Consider rate-limiting the per-drop warning log.

The non-blocking send correctly prevents goroutine stalls. However, logging a warning for every dropped result can cause log spam during sustained buffer overflow. Based on the call sites in runner.go (lines 220-230, 353-359, 414-417), tight loops can generate thousands of results, potentially flooding logs with warnings.

Since the total dropped count is already reported at termination (line 135), consider either:

  • Removing the per-drop warning entirely, or
  • Rate-limiting it (e.g., log once per N drops or once per time interval)
♻️ Option: Log only on first drop, rely on final summary
 	return func(resp *clients.Response) {
 		select {
 		case u.data <- resp:
 		default:
-			u.droppedCounter.Add(1)
-			gologger.Warning().Msgf("PDCP upload buffer full, skipping result")
+			if u.droppedCounter.Add(1) == 1 {
+				gologger.Warning().Msgf("PDCP upload buffer full, some results will be skipped (total reported at end)")
+			}
 		}
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/pdcp/writer.go` around lines 95 - 100, The per-drop gologger.Warning
in the non-blocking send (sending to u.data with default branch) causes log
spam; change it to either remove the warning or rate-limit it: add a small
logging guard in the uploader (e.g., track a counter or timestamp on the
uploader struct) and only call gologger.Warning().Msgf("PDCP upload buffer full,
skipping result") on the first drop or once per N drops / per time interval; use
u.droppedCounter (or an atomic/int field like lastDropLog or dropsSinceLastLog)
to decide when to log so the drop increment stays but the warning is emitted
rarely. Ensure the conditional surrounds the existing gologger call and preserve
the u.droppedCounter.Add(1) increment and non-blocking send behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/tlsx/tls/tls.go`:
- Around line 239-243: EnumerateCiphers currently creates a background context
and only sets a deadline if c.options.Timeout != 0, causing
conn.HandshakeContext to potentially block indefinitely; change the logic in
EnumerateCiphers (around ctx := context.Background(), pool.Acquire and
conn.HandshakeContext) to treat c.options.Timeout == 0 as a 5-second default
(the same default used by GetConn()), i.e. create a context.WithTimeout(ctx,
time.Duration(timeout)*time.Second) where timeout := c.options.Timeout; if
timeout == 0 set timeout = 5, and ensure you call the cancel function defer
cancel() after creating the context so the deadline is always enforced and
resources are released.

In `@pkg/tlsx/ztls/ztls.go`:
- Around line 332-347: tlsHandshakeWithTimeout returns on context timeout while
the goroutine running tlsConn.Handshake may still reference a mutable tls.Config
(created via tls.Client using baseCfg), which allows concurrent mutation of
baseCfg.CipherSuites to race with the in-flight handshake; fix by cloning the
tls.Config for each handshake attempt so the goroutine uses an immutable copy
(i.e., create a shallow copy of baseCfg or use baseCfg.Clone() before calling
tls.Client/handshake) and pass that cloned config to tls.Client/tlsConn so
mutations to baseCfg.CipherSuites in later iterations cannot affect the running
handshake.

---

Duplicate comments:
In `@pkg/tlsx/ztls/ztls.go`:
- Around line 260-264: The cipher enumeration uses context.Background() when
c.options.Timeout == 0, which allows hangs; change this so that when
c.options.Timeout == 0 you create a context with a 5-second timeout instead (use
context.WithTimeout(ctx, 5*time.Second)) and ensure the resulting cancel
function is assigned and deferred; update the logic around ctx, cancel in the
ztls cipher enumeration code (referencing c.options.Timeout, ctx and cancel in
ztls.go) so the default timeout follows the project convention.

---

Nitpick comments:
In `@internal/pdcp/writer.go`:
- Around line 95-100: The per-drop gologger.Warning in the non-blocking send
(sending to u.data with default branch) causes log spam; change it to either
remove the warning or rate-limit it: add a small logging guard in the uploader
(e.g., track a counter or timestamp on the uploader struct) and only call
gologger.Warning().Msgf("PDCP upload buffer full, skipping result") on the first
drop or once per N drops / per time interval; use u.droppedCounter (or an
atomic/int field like lastDropLog or dropsSinceLastLog) to decide when to log so
the drop increment stays but the warning is emitted rarely. Ensure the
conditional surrounds the existing gologger call and preserve the
u.droppedCounter.Add(1) increment and non-blocking send behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45e33b6f-d3a9-4a56-a2b5-2b61f2108cab

📥 Commits

Reviewing files that changed from the base of the PR and between 37436e1 and 679cf13.

📒 Files selected for processing (3)
  • internal/pdcp/writer.go
  • pkg/tlsx/tls/tls.go
  • pkg/tlsx/ztls/ztls.go

Comment on lines +239 to +243
ctx := context.Background()
var cancel context.CancelFunc
if c.options.Timeout != 0 {
ctx, cancel = context.WithTimeout(ctx, time.Duration(c.options.Timeout)*time.Second)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file structure and locate the relevant files
fd -t f "tls\.go|utils\.go" | grep -E "(pkg/tlsx|tlsx)" | head -20

Repository: projectdiscovery/tlsx

Length of output: 203


🏁 Script executed:

# Read the actual code at lines 239-243 and surrounding context
cat -n pkg/tlsx/tls/tls.go | sed -n '230,260p'

Repository: projectdiscovery/tlsx

Length of output: 1252


🏁 Script executed:

# Find and examine the clients/utils.go file to understand the timeout default
fd -t f "utils\.go" | xargs grep -l "Timeout" | grep clients

Repository: projectdiscovery/tlsx

Length of output: 90


🏁 Script executed:

# Read the clients/utils.go file
cat -n pkg/tlsx/clients/utils.go

Repository: projectdiscovery/tlsx

Length of output: 5897


🏁 Script executed:

# Search for Timeout handling and GetConn function in clients
rg -A 10 -B 5 "Timeout.*==.*0|GetConn|5.*time\.Second" pkg/tlsx/clients/

Repository: projectdiscovery/tlsx

Length of output: 2422


🏁 Script executed:

# Check imports in tls.go to verify ProjectDiscovery library usage
head -50 pkg/tlsx/tls/tls.go | grep -E "^import|^\"" | head -30

Repository: projectdiscovery/tlsx

Length of output: 73


🏁 Script executed:

# Get full imports section
cat -n pkg/tlsx/tls/tls.go | sed -n '1,30p'

Repository: projectdiscovery/tlsx

Length of output: 1067


🏁 Script executed:

# Final verification: Check if there are any other places in EnumerateCiphers that handle timeout
cat -n pkg/tlsx/tls/tls.go | sed -n '200,255p'

Repository: projectdiscovery/tlsx

Length of output: 2120


Add default handshake timeout to cipher enumeration to prevent indefinite blocking.

pkg/tlsx/clients/utils.go treats Timeout == 0 as a 5-second default via GetConn(). However, EnumerateCiphers uses pool.Acquire() directly, bypassing this default. Without a context deadline, conn.HandshakeContext() can hang indefinitely if a peer accepts TCP but never completes the TLS handshake.

Proposed fix
-		ctx := context.Background()
-		var cancel context.CancelFunc
-		if c.options.Timeout != 0 {
-			ctx, cancel = context.WithTimeout(ctx, time.Duration(c.options.Timeout)*time.Second)
-		}
+		timeout := time.Duration(c.options.Timeout) * time.Second
+		if timeout == 0 {
+			timeout = 5 * time.Second
+		}
+		ctx, cancel := context.WithTimeout(context.Background(), timeout)
 		if err := conn.HandshakeContext(ctx); err == nil {
 			ciphersuite := conn.ConnectionState().CipherSuite
 			enumeratedCiphers = append(enumeratedCiphers, tls.CipherSuiteName(ciphersuite))
 		}
-		if cancel != nil {
-			cancel()
-		}
+		cancel()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/tlsx/tls/tls.go` around lines 239 - 243, EnumerateCiphers currently
creates a background context and only sets a deadline if c.options.Timeout != 0,
causing conn.HandshakeContext to potentially block indefinitely; change the
logic in EnumerateCiphers (around ctx := context.Background(), pool.Acquire and
conn.HandshakeContext) to treat c.options.Timeout == 0 as a 5-second default
(the same default used by GetConn()), i.e. create a context.WithTimeout(ctx,
time.Duration(timeout)*time.Second) where timeout := c.options.Timeout; if
timeout == 0 set timeout = 5, and ensure you call the cancel function defer
cancel() after creating the context so the deadline is always enforced and
resources are released.

Comment on lines +332 to 347
func (c *Client) tlsHandshakeWithTimeout(ctx context.Context, tlsConn *tls.Conn) error {
errChan := make(chan error, 1)
defer close(errChan)

go func() {
errChan <- tlsConn.Handshake()
}()

select {
case <-ctx.Done():
return errorutil.NewWithTag("ztls", "timeout while attempting handshake") //nolint
case errChan <- tlsConn.Handshake():
}

err := <-errChan
if err == tls.ErrCertsOnly {
err = nil
case err := <-errChan:
if err == tls.ErrCertsOnly {
err = nil
}
return err
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to see the code at lines 332-347 and surrounding context
wc -l pkg/tlsx/ztls/ztls.go

Repository: projectdiscovery/tlsx

Length of output: 90


🏁 Script executed:

# Read the specific section mentioned in the review
sed -n '320,360p' pkg/tlsx/ztls/ztls.go

Repository: projectdiscovery/tlsx

Length of output: 816


🏁 Script executed:

# Search for EnumerateCiphers function and how baseCfg.CipherSuites is used
rg -A 30 "func.*EnumerateCiphers" pkg/tlsx/ztls/

Repository: projectdiscovery/tlsx

Length of output: 1792


🏁 Script executed:

# Search for how tlsHandshakeWithTimeout is called and how baseCfg is passed
rg -B 10 "tlsHandshakeWithTimeout" pkg/tlsx/ztls/

Repository: projectdiscovery/tlsx

Length of output: 1835


🏁 Script executed:

# Get more context around the tlsHandshakeWithTimeout call in EnumerateCiphers
rg -B 5 -A 15 "conn := tls.Client.*baseConn.*baseCfg" pkg/tlsx/ztls/

Repository: projectdiscovery/tlsx

Length of output: 1274


🏁 Script executed:

# Search for the loop that contains this code
rg -B 30 "baseCfg.CipherSuites = \[\]uint16" pkg/tlsx/ztls/

Repository: projectdiscovery/tlsx

Length of output: 1860


🏁 Script executed:

# Check if there's any defer cancel() or cleanup that ensures goroutines are waited on
sed -n '140,280p' pkg/tlsx/ztls/ztls.go

Repository: projectdiscovery/tlsx

Length of output: 4787


🏁 Script executed:

# Check for go.mod or go version requirements
cat go.mod | head -5

Repository: projectdiscovery/tlsx

Length of output: 126


🏁 Script executed:

# Check tls.Config.Clone() documentation from Go standard library
web_search
Go tls.Config Clone() method standard library

Repository: projectdiscovery/tlsx

Length of output: 246


🌐 Web query:

Go crypto/tls Config Clone() method availability version

💡 Result:

(*crypto/tls.Config).Clone() is part of the Go standard library starting in Go 1.8 (“added in go1.8” in the official crypto/tls docs), and is therefore available in Go 1.8+. [1]


Clone the config to prevent race conditions in cipher enumeration.

On timeout, tlsHandshakeWithTimeout returns immediately while the handshake goroutine continues running. Since tls.Client() holds a reference to the config (not a copy), the next iteration's mutation of baseCfg.CipherSuites can affect the in-flight handshake, causing enumeration flakiness under timeout pressure.

Fix: Clone config for each attempt
-		conn := tls.Client(baseConn, baseCfg)
-		baseCfg.CipherSuites = []uint16{ztlsCiphers[v]}
+		cfg := baseCfg.Clone()
+		cfg.CipherSuites = []uint16{ztlsCiphers[v]}
+		conn := tls.Client(baseConn, cfg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/tlsx/ztls/ztls.go` around lines 332 - 347, tlsHandshakeWithTimeout
returns on context timeout while the goroutine running tlsConn.Handshake may
still reference a mutable tls.Config (created via tls.Client using baseCfg),
which allows concurrent mutation of baseCfg.CipherSuites to race with the
in-flight handshake; fix by cloning the tls.Config for each handshake attempt so
the goroutine uses an immutable copy (i.e., create a shallow copy of baseCfg or
use baseCfg.Clone() before calling tls.Client/handshake) and pass that cloned
config to tls.Client/tlsConn so mutations to baseCfg.CipherSuites in later
iterations cannot affect the running handshake.

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