fix: prevent indefinite hangs during TLS operations#891
fix: prevent indefinite hangs during TLS operations#891robutrader wants to merge 2 commits intoprojectdiscovery:mainfrom
Conversation
Fixes projectdiscovery#819 ## Summary This PR fixes the issue where tlsx hangs indefinitely when processing large target lists (~25k+ hosts). The output gets cut mid-line (e.g., mid-JSON key) because the process hangs during a write operation. ## Root Causes & Fixes ### 1. Broken timeout in ztls handshake The tlsHandshakeWithTimeout function ran the handshake inline in a select statement instead of a goroutine, making the timeout ineffective. The handshake would block forever for unresponsive servers. **Fix:** Run handshake in a goroutine so the select can properly choose between handshake completion and context timeout. Close the connection on timeout to unblock the goroutine. ### 2. Missing timeout in cipher enumeration Both ztls and tls EnumerateCiphers methods called Handshake() without any timeout context, causing indefinite hangs during cipher enumeration. **Fix:** Added proper timeout context using c.options.Timeout for all cipher enumeration handshakes. ### 3. No periodic flush for file output The file writer only flushed on Close(), meaning data buffered in memory would be lost if the process hung or crashed. **Fix:** Added periodic flush mechanism triggered every 100 writes to minimize data loss on unexpected termination. ## Testing - Added unit tests for timeout cancellation behavior - Added unit tests for periodic flush mechanism - All tests pass with race detector enabled - go vet passes with no issues
WalkthroughAdds a periodic flush counter to the file writer (flush every 100 writes) and implements context-aware TLS handshakes with per-iteration deadlines and goroutine-driven handshake execution to avoid indefinite hangs. Changes
Sequence Diagram(s)sequenceDiagram
actor Enum as EnumerateCiphers
participant Ctx as Context (with timeout)
participant Handler as tlsHandshakeWithTimeout
participant GR as Goroutine (handshake)
participant Conn as TLS Conn
Enum->>Ctx: Create per-iteration context (options.Timeout)
Enum->>Handler: Call tlsHandshakeWithTimeout(conn, ctx)
Handler->>GR: Spawn goroutine to run conn.Handshake()
par
GR->>Conn: Perform Handshake
Conn-->>GR: Handshake result (success / error)
and
Handler->>Ctx: Monitor ctx.Done()
Ctx-->>Handler: Timeout / cancel signal
end
alt Handshake completes first
GR-->>Handler: return result
Handler-->>Enum: success -> record cipher / or error -> continue
else Context times out first
Handler->>Conn: Close connection to unblock goroutine
Handler-->>Enum: return timeout error -> continue enumeration
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@pkg/tlsx/tls/tls.go`:
- Around line 239-245: The loop creates a timeout context with ctx, cancel :=
context.WithTimeout(...) and defers cancel(), which retains all timers until the
function exits; change this so you do not defer cancel() inside the loop—only
call cancel() explicitly at the end of each iteration after the handshake
completes (and only when c.options.Timeout != 0 / cancel is non-nil), ensuring
you don't shadow variables (use the existing ctx variable or a new scoped one)
so each iteration cancels its timer immediately instead of accumulating
goroutines.
In `@pkg/tlsx/ztls/ztls.go`:
- Around line 260-266: The current cipher-enumeration handshake uses
context.WithTimeout and defers cancel(), which delays timer cancelation until
function return; instead, inside the cipher enumeration loop create the
per-iteration context (ctx) and cancel func when c.options.Timeout != 0, pass
ctx into tlsHandshakeWithTimeout, and explicitly call cancel() immediately after
the tlsHandshakeWithTimeout call (regardless of outcome) rather than deferring
it; adjust use of ctx/tlsHandshakeWithTimeout/c.options.Timeout so each
iteration releases its timer promptly.
🧹 Nitpick comments (1)
pkg/output/file_writer_test.go (1)
1-77: Consider adding edge case tests.The tests effectively cover the happy path for periodic flush, write counting, and close behavior. For more comprehensive coverage, consider adding:
- A test verifying the exact line count in the flushed file matches the number of writes.
- A test for multiple flush cycles (e.g., 250 writes should trigger 2 flushes).
Address CodeRabbit review feedback: defer cancel() in loops keeps all timers alive until function exit. Call cancel() explicitly after each handshake iteration to release resources immediately. Affected files: - pkg/tlsx/tls/tls.go - pkg/tlsx/ztls/ztls.go
|
Thanks for the contribution! This has been resolved by #949 which was merged into dev. Your fix had good scope covering the handshake and cipher enumeration in both TLS backends. The remaining issue was closing |
Fixes #819
Summary
This PR fixes the issue where tlsx hangs indefinitely when processing large target lists (~25k+ hosts). The output gets cut mid-line (e.g., mid-JSON key) because the process hangs during a write operation.
Root Causes & Fixes
1. Broken timeout in ztls handshake
The tlsHandshakeWithTimeout function ran the handshake inline in a select statement instead of a goroutine, making the timeout ineffective. The handshake would block forever for unresponsive servers.
Fix: Run handshake in a goroutine so the select can properly choose between handshake completion and context timeout. Close the connection on timeout to unblock the goroutine.
2. Missing timeout in cipher enumeration
Both ztls and tls EnumerateCiphers methods called Handshake() without any timeout context, causing indefinite hangs during cipher enumeration.
Fix: Added proper timeout context using c.options.Timeout for all cipher enumeration handshakes.
3. No periodic flush for file output
The file writer only flushed on Close(), meaning data buffered in memory would be lost if the process hung or crashed.
Fix: Added periodic flush mechanism triggered every 100 writes to minimize data loss on unexpected termination.
Testing
Summary by CodeRabbit
New Features
Tests