Skip to content

fix: prevent indefinite hangs in TLS handshake and cipher enumeration#933

Closed
Bushi-gg wants to merge 3 commits intoprojectdiscovery:devfrom
Bushi-gg:fix/819-tls-handshake-hang
Closed

fix: prevent indefinite hangs in TLS handshake and cipher enumeration#933
Bushi-gg wants to merge 3 commits intoprojectdiscovery:devfrom
Bushi-gg:fix/819-tls-handshake-hang

Conversation

@Bushi-gg
Copy link

Fixes #819

What's happening

tlsx hangs indefinitely on certain hosts during cipher enumeration. I traced it to a few things:

tlsHandshakeWithTimeout doesn't actually time out (ztls.go)

The select runs Handshake() inline as the case expression, so if it blocks, the timeout case is never reached:

select {
case <-ctx.Done():
    return error // dead code when Handshake blocks
case errChan <- tlsConn.Handshake():
    // blocks the entire select
}

Moved Handshake() into a goroutine so the select can actually fire on ctx.Done(). On timeout, tlsConn.Close() unblocks the goroutine.

context.TODO() passed to the handshake (ztls.go)

Even with the select fix, EnumerateCiphers was passing context.TODO() which never cancels. Replaced with a context.WithTimeout per cipher iteration using options.Timeout.

No timeout at all in ctls path (tls.go)

The crypto/tls EnumerateCiphers called conn.Handshake() directly — no context, no timeout. Switched to conn.HandshakeContext(ctx) with the same per-iteration timeout.

pool.Acquire hangs on unresponsive hosts

Both EnumerateCiphers paths called pool.Acquire(context.Background()), which can also block indefinitely if the host accepts TCP but stalls. Now uses the same timeout context for pool acquisition.

Config mutation across loop iterations

baseCfg.CipherSuites was mutated in-place each iteration. Added baseCfg.Clone() per iteration to avoid sharing state.

Both cipher enumeration paths include a guard for zero/negative timeout values — falls back to a cancelable context instead of failing immediately.

Files changed

  • pkg/tlsx/ztls/ztls.go — fix select, replace context.TODO, pool.Acquire timeout, config clone
  • pkg/tlsx/tls/tls.go — HandshakeContext, pool.Acquire timeout, config clone
  • pkg/tlsx/ztls/ztls_test.go — regression test with a TCP server that never responds

Test

$ go test -v -run TestHandshakeTimeout ./pkg/tlsx/ztls/
=== RUN   TestHandshakeTimeout
    ztls_test.go:163: ConnectWithOptions returned in 2.001s (timeout=2s) — OK
--- PASS: TestHandshakeTimeout (2.00s)
PASS

The test starts a TCP listener that accepts but never sends data (simulating the hung handshake from #819). Before the fix it blocks forever. After the fix it returns within the configured timeout.

Checklist

  • PR created against the correct branch (usually dev)
  • All checks passed (lint, unit/integration/regression tests)
  • Tests added that prove the fix is effective
  • Documentation added (if appropriate) — no docs change needed

- Wrap pool.Acquire() with the same timeout context so connection pool
  acquisition cannot hang indefinitely on unresponsive hosts
- Clone baseCfg per cipher iteration to prevent concurrent mutation of
  the shared CipherSuites slice
- Applied consistently to both tls.go and ztls.go EnumerateCiphers
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Feb 28, 2026

Neo Security Audit

No security issues found

Highlights

  • The goroutine spawned in tlsHandshakeWithTimeout (line 336) is properly cleaned up via buffered channel and tlsConn.Close() on timeout
  • Context cancellation is correctly handled in all code paths (success, error, timeout) in both EnumerateCiphers implementations
Hardening Notes
  • The goroutine spawned in tlsHandshakeWithTimeout (line 336) is properly cleaned up via buffered channel and tlsConn.Close() on timeout
  • Context cancellation is correctly handled in all code paths (success, error, timeout) in both EnumerateCiphers implementations
  • Config cloning (baseCfg.Clone()) prevents shared state mutation across concurrent cipher enumeration iterations
  • Test coverage validates timeout behavior with a mock server that simulates hanging handshakes

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

@Bushi-gg Bushi-gg force-pushed the fix/819-tls-handshake-hang branch from 45bad36 to 6477c2a Compare March 1, 2026 08:22
@dogancanbakir
Copy link
Member

Hi, thanks for your interest in contributing! Just a heads up, we ask contributors to work on 1 active issue at a time (see).

Also, we welcome AI-assisted development, but submissions must be complete, tested, and ready to merge. Please also make sure to fill out the PR template with proof that your changes work.

We're closing this PR along with your other open submissions. Once you're ready, feel free to pick one issue to focus on and resubmit; we'd be happy to review it.

Appreciate your understanding!

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.

2 participants