René's URL Explorer Experiment


Title: Assertion failure crash in TLSWrap::DoWrite with zombie HTTP/2 session (close event not propagated from OS-level CLOSED socket) · Issue #61304 · nodejs/node · GitHub

Open Graph Title: Assertion failure crash in TLSWrap::DoWrite with zombie HTTP/2 session (close event not propagated from OS-level CLOSED socket) · Issue #61304 · nodejs/node

X Title: Assertion failure crash in TLSWrap::DoWrite with zombie HTTP/2 session (close event not propagated from OS-level CLOSED socket) · Issue #61304 · nodejs/node

Description: Version v22.13.1 Platform Darwin 24.3.0 (macOS, arm64) Subsystem http2, tls What steps will reproduce the bug? Reproducible test case (requires sudo for firewall manipulation) We created a test that simulates a network "black hole" using...

Open Graph Description: Version v22.13.1 Platform Darwin 24.3.0 (macOS, arm64) Subsystem http2, tls What steps will reproduce the bug? Reproducible test case (requires sudo for firewall manipulation) We created a test tha...

X Description: Version v22.13.1 Platform Darwin 24.3.0 (macOS, arm64) Subsystem http2, tls What steps will reproduce the bug? Reproducible test case (requires sudo for firewall manipulation) We created a test tha...

Opengraph URL: https://github.com/nodejs/node/issues/61304

X: @github

direct link

Domain: github.com


Hey, it has json ld scripts:
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Assertion failure crash in TLSWrap::DoWrite with zombie HTTP/2 session (close event not propagated from OS-level CLOSED socket)","articleBody":"# Version\n\nv22.13.1\n\n# Platform\n\nDarwin 24.3.0 (macOS, arm64)\n\n# Subsystem\n\nhttp2, tls\n\n# What steps will reproduce the bug?\n\n## Reproducible test case (requires sudo for firewall manipulation)\n\nWe created a test that simulates a network \"black hole\" using macOS pf firewall:\n\n```javascript\n// test-zombie-blackhole.js\n// Run with: sudo node test-zombie-blackhole.js\n\nconst http2 = require('http2')\nconst { spawn, execSync } = require('child_process')\n\nconst PORT = 8444\n\nasync function main() {\n    if (process.getuid() !== 0) {\n        console.log('Run with: sudo node test-zombie-blackhole.js')\n        process.exit(1)\n    }\n\n    // Start server as child process\n    const server = spawn('node', ['-e', `\n        const http2 = require('http2')\n        const fs = require('fs')\n        const server = http2.createSecureServer({\n            key: fs.readFileSync('./key.pem'),\n            cert: fs.readFileSync('./cert.pem'),\n        })\n        server.on('stream', (s, h) =\u003e {\n            s.respond({ ':status': 200 })\n            s.end('ok')\n        })\n        server.listen(${PORT}, () =\u003e console.log('Server ready'))\n        setInterval(() =\u003e {}, 10000)\n    `], { stdio: 'inherit' })\n\n    await new Promise(r =\u003e setTimeout(r, 1000))\n\n    // Connect client\n    const session = http2.connect(\\`https://localhost:\\${PORT}\\`, { rejectUnauthorized: false })\n\n    session.on('error', e =\u003e console.log('error event:', e.message))\n    session.on('close', () =\u003e console.log('close event'))\n\n    await new Promise(r =\u003e session.on('connect', r))\n    console.log('Connected')\n\n    // Make initial request to establish session\n    await new Promise((resolve, reject) =\u003e {\n        const s = session.request({ ':path': '/init' })\n        s.on('end', resolve)\n        s.on('error', reject)\n        s.end()\n    })\n\n    // Block traffic with firewall (black hole - no RST/FIN, packets just disappear)\n    execSync(\\`echo \"block drop quick proto tcp from any to 127.0.0.1 port \\${PORT}\" | pfctl -a zombie_test -f -\\`)\n    execSync('pfctl -e 2\u003e/dev/null || true')\n    console.log('Firewall blocking traffic')\n\n    // Wait - session should remain \"healthy\" looking\n    await new Promise(r =\u003e setTimeout(r, 5000))\n\n    console.log('Session state:', {\n        closed: session.closed,\n        destroyed: session.destroyed,\n        queueSize: session.state?.outboundQueueSize\n    })\n\n    // Attempt write to trigger crash\n    const syms = Object.getOwnPropertySymbols(session)\n    const sockSym = syms.find(s =\u003e s.toString().includes('socket'))\n    const tls = sockSym ? session[sockSym] : session.socket\n\n    console.log('Writing to socket...')\n    tls.write('test')  // CRASHES HERE\n\n    // Cleanup (won't reach here)\n    execSync('pfctl -a zombie_test -F all')\n    server.kill()\n}\n\nmain()\n```\n\n## What happens\n\n1. Server and client establish HTTP/2 connection over TLS\n2. Firewall rule creates a \"black hole\" (packets dropped, no RST/FIN sent)\n3. Session continues to report `closed: false`, `destroyed: false`\n4. No error or close events fire\n5. Writing to the TLS socket triggers an assertion failure crash\n\n## Original discovery\n\nThis was originally discovered in a long-running production process (~2 days) where the TCP socket entered CLOSED state at the OS level (visible via `lsof`) but Node.js never received the close event.\n\n# How often does it reproduce? Is there a required condition?\n\n**100% reproducible** with the firewall-based test above.\n\nIn production, it's intermittent and requires:\n- Long-running HTTP/2 session\n- Network event that causes packet loss without proper TCP RST/FIN (NAT timeout, network partition, etc.)\n\n# What is the expected behavior? Why is that the expected behavior?\n\n1. **Close events should propagate** - When the OS-level TCP socket enters CLOSED state, this should propagate up through TLS and HTTP/2 layers, setting `session.closed = true` and emitting appropriate events\n\n2. **Write should fail gracefully** - Even if the zombie state occurs, calling `.write()` should return an error via callback, not crash with an assertion failure\n\n# What do you see instead?\n\n1. **Close event is lost** - All layers above TCP continue to report healthy status\n2. **Writes queue up** - `session.state.outboundQueueSize` grows indefinitely (we observed 2815 queued frames)\n3. **Crash on write** - Calling `.write()` crashes the process with assertion failure\n\n# Detailed debugging evidence\n\nWe attached Chrome DevTools inspector to the running process and gathered the following:\n\n**OS level** (via `lsof -p \u003cpid\u003e`):\n```\nnode \u003cpid\u003e 2128u IPv4 ... TCP ...:62689-\u003e...:https (CLOSED)\n```\n\nThe file descriptor 2128 is in **CLOSED** state at the OS level.\n\n**Node level** (via inspector):\n```javascript\nsessionInfo.session.closed      // false - thinks it's open!\nsessionInfo.session.destroyed   // false\nsessionInfo.session.connecting  // false\n\n// TLS socket also reports healthy:\nsocket.destroyed   // false\nsocket.readable    // true\nsocket.writable    // true\nsocket.readyState  // 'open'\n\n// But the outbound queue is stuck:\nsession.state.outboundQueueSize // 2815 frames queued!\nsessionInfo.pendingRejects.size // 3 requests waiting\n\n// The TLS socket's underlying TCP handle IS the CLOSED fd:\nsocket._handle._parent.fd       // 2128 (the CLOSED socket!)\n```\n\n**Ping test** (callback never fires):\n```javascript\nsessionInfo.session.ping((err, duration) =\u003e console.log(err, duration))\n// Returns true (ping \"sent\") but callback NEVER executes\n```\n\n**Fresh connection to same host works fine:**\n```javascript\nrequire('http2').connect('https://same-host.com').ping((e,d) =\u003e console.log(e,d))\n// -\u003e connected!\n// -\u003e ping: null 19.748583 (success, 20ms latency)\n```\n\nThis proves the server is reachable; only the cached zombie session is broken.\n\n# Crash output\n\nWe've observed two different assertion failures depending on the scenario:\n\n## Crash 1: From reproducible test (firewall black hole)\n\n```\n#  node[7869]: virtual void node::http2::Http2Session::OnStreamAfterWrite(node::WriteWrap *, int) at ../src/node_http2.cc:1741\n#  Assertion failed: is_write_in_progress()\n\n----- Native stack trace -----\n\n 1: 0x1043e8d1c node::Assert(node::AssertionInfo const\u0026)\n 2: 0x10601d89c node::http2::Http2Session::OnStreamAfterWrite(node::WriteWrap*, int) (.cold.1)\n 3: 0x10441a9a4 node::http2::Http2Session::ClearOutgoing(int)\n 4: 0x1044f8520 node::WriteWrap::OnDone(int)\n 5: 0x1044f8848 node::StreamReq::Done(int, char const*)\n 6: 0x104576f2c node::crypto::TLSWrap::InvokeQueued(int, char const*)\n 7: 0x104578c88 node::crypto::TLSWrap::OnStreamAfterWrite(node::WriteWrap*, int)\n...\n```\n\n## Crash 2: From production debugging (long-running zombie session)\n\n```\n#  node[71801]: virtual int node::crypto::TLSWrap::DoWrite(node::WriteWrap *, uv_buf_t *, size_t, uv_stream_t *) at ../src/crypto/crypto_tls.cc:1033\n#  Assertion failed: !current_write_\n\n----- Native stack trace -----\n\n 1: 0x102978d1c node::Assert(node::AssertionInfo const\u0026)\n 2: 0x1045de9bc node::crypto::TLSWrap::DoWrite(node::WriteWrap*, uv_buf_t*, unsigned long, uv_stream_s*) (.cold.8)\n 3: 0x102b09e24 node::crypto::TLSWrap::DoWrite(node::WriteWrap*, uv_buf_t*, unsigned long, uv_stream_s*)\n 4: 0x102a85198 node::StreamBase::Write(uv_buf_t*, unsigned long, uv_stream_s*, v8::Local\u003cv8::Object\u003e, bool)\n 5: 0x102a89288 int node::StreamBase::WriteString\u003c(node::encoding)1\u003e(v8::FunctionCallbackInfo\u003cv8::Value\u003e const\u0026)\n...\n\n----- JavaScript stack trace -----\n\n1: handleWriteReq (node:internal/stream_base_commons:62:21)\n2: writeGeneric (node:internal/stream_base_commons:148:15)\n3: Socket._writeGeneric (node:net:971:11)\n4: Socket._write (node:net:983:8)\n5: writeOrBuffer (node:internal/streams/writable:572:12)\n6: _write (node:internal/streams/writable:501:10)\n7: Writable.write (node:internal/streams/writable:510:10)\n```\n\nBoth crashes indicate internal state corruption in the TLS/HTTP2 layers when the underlying connection is silently broken.\n\n# Relationship to existing issues\n\nThis appears related to but distinct from previously fixed issues:\n\n- **#49307** (HTTP/2 segfault if underlying socket is unexpectedly closed) - Fixed via PR #49327. That fix handles the case where the socket is *destroyed*, but our scenario involves a socket in CLOSED state at the OS level where **no close/error event ever propagated to Node.js**.\n\n- **#30896** (TLS assertion error in DoWrite) - Same assertion failure `!current_write_`, attributed to memory exhaustion. Our case is not memory-related; it's a zombie session with a silently-closed TCP connection.\n\n- **[PR #18987](https://github.com/nodejs/node/pull/18987)** (Handle writes after SSL destroy more gracefully) - This fix handles writes after SSL is destroyed, but in our case the SSL layer doesn't know it should be destroyed.\n\nThe key difference in our scenario: **the OS socket is CLOSED but Node.js never received the close event**, leaving the TLS and HTTP/2 layers in an inconsistent state where they believe the connection is healthy.\n\n# Additional information\n\nThe zombie session persisted for an extended period (potentially hours) before we discovered it via debugging. All requests to the affected origin silently failed (queued but never sent), while requests to other origins continued to work normally.\n\nThe `session.state.outboundQueueSize` growing while `bytesWritten` remains static is a clear indicator of this zombie state, but there's no documented way to detect this condition - all public APIs (`session.closed`, `socket.writable`, etc.) report the connection as healthy.\n","author":{"url":"https://github.com/dglittle","@type":"Person","name":"dglittle"},"datePublished":"2026-01-07T07:48:30.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/61304/node/issues/61304"}

route-pattern/_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format)
route-controllervoltron_issues_fragments
route-actionissue_layout
fetch-noncev2:0081751f-d3f8-794f-ea2a-a623b2a43b64
current-catalog-service-hash81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114
request-idC87E:133E02:254EAA4:312431E:69648662
html-safe-noncedbef396eae31e070cb25dead8f80e15c11c9be3281258b8deaf1edb9edf89e40
visitor-payloadeyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDODdFOjEzM0UwMjoyNTRFQUE0OjMxMjQzMUU6Njk2NDg2NjIiLCJ2aXNpdG9yX2lkIjoiNTA2NzA4NTQ5MTM5OTMyOTM3OCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9
visitor-hmac5a61ad969e3ff9901418bd7eac90c28b39c580d8cf9e8cafeb6b3bd3961382d5
hovercard-subject-tagissue:3787716236
github-keyboard-shortcutsrepository,issues,copilot
google-site-verificationApib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I
octolytics-urlhttps://collector.github.com/github/collect
analytics-location///voltron/issues_fragments/issue_layout
fb:app_id1401488693436528
apple-itunes-appapp-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/61304/issue_layout
twitter:imagehttps://opengraph.githubassets.com/4403077ede4cd4da093c7d3912ee662a940c05bd2d3c4ba9af1c6473b7390606/nodejs/node/issues/61304
twitter:cardsummary_large_image
og:imagehttps://opengraph.githubassets.com/4403077ede4cd4da093c7d3912ee662a940c05bd2d3c4ba9af1c6473b7390606/nodejs/node/issues/61304
og:image:altVersion v22.13.1 Platform Darwin 24.3.0 (macOS, arm64) Subsystem http2, tls What steps will reproduce the bug? Reproducible test case (requires sudo for firewall manipulation) We created a test tha...
og:image:width1200
og:image:height600
og:site_nameGitHub
og:typeobject
og:author:usernamedglittle
hostnamegithub.com
expected-hostnamegithub.com
Nonebaa7d9900fdf7b27d604f36887af878d569cfbdcf97126832a5f4f0caf0c6ba5
turbo-cache-controlno-preview
go-importgithub.com/nodejs/node git https://github.com/nodejs/node.git
octolytics-dimension-user_id9950313
octolytics-dimension-user_loginnodejs
octolytics-dimension-repository_id27193779
octolytics-dimension-repository_nwonodejs/node
octolytics-dimension-repository_publictrue
octolytics-dimension-repository_is_forkfalse
octolytics-dimension-repository_network_root_id27193779
octolytics-dimension-repository_network_root_nwonodejs/node
turbo-body-classeslogged-out env-production page-responsive
disable-turbofalse
browser-stats-urlhttps://api.github.com/_private/browser/stats
browser-errors-urlhttps://api.github.com/_private/browser/errors
release842eff1d11f899d02b6b3b98fa3ea4860e64b34e
ui-targetfull
theme-color#1e2327
color-schemelight dark

Links:

Skip to contenthttps://github.com/nodejs/node/issues/61304#start-of-content
https://github.com/
Sign in https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fnodejs%2Fnode%2Fissues%2F61304
GitHub CopilotWrite better code with AIhttps://github.com/features/copilot
GitHub SparkBuild and deploy intelligent appshttps://github.com/features/spark
GitHub ModelsManage and compare promptshttps://github.com/features/models
MCP RegistryNewIntegrate external toolshttps://github.com/mcp
ActionsAutomate any workflowhttps://github.com/features/actions
CodespacesInstant dev environmentshttps://github.com/features/codespaces
IssuesPlan and track workhttps://github.com/features/issues
Code ReviewManage code changeshttps://github.com/features/code-review
GitHub Advanced SecurityFind and fix vulnerabilitieshttps://github.com/security/advanced-security
Code securitySecure your code as you buildhttps://github.com/security/advanced-security/code-security
Secret protectionStop leaks before they starthttps://github.com/security/advanced-security/secret-protection
Why GitHubhttps://github.com/why-github
Documentationhttps://docs.github.com
Bloghttps://github.blog
Changeloghttps://github.blog/changelog
Marketplacehttps://github.com/marketplace
View all featureshttps://github.com/features
Enterpriseshttps://github.com/enterprise
Small and medium teamshttps://github.com/team
Startupshttps://github.com/enterprise/startups
Nonprofitshttps://github.com/solutions/industry/nonprofits
App Modernizationhttps://github.com/solutions/use-case/app-modernization
DevSecOpshttps://github.com/solutions/use-case/devsecops
DevOpshttps://github.com/solutions/use-case/devops
CI/CDhttps://github.com/solutions/use-case/ci-cd
View all use caseshttps://github.com/solutions/use-case
Healthcarehttps://github.com/solutions/industry/healthcare
Financial serviceshttps://github.com/solutions/industry/financial-services
Manufacturinghttps://github.com/solutions/industry/manufacturing
Governmenthttps://github.com/solutions/industry/government
View all industrieshttps://github.com/solutions/industry
View all solutionshttps://github.com/solutions
AIhttps://github.com/resources/articles?topic=ai
Software Developmenthttps://github.com/resources/articles?topic=software-development
DevOpshttps://github.com/resources/articles?topic=devops
Securityhttps://github.com/resources/articles?topic=security
View all topicshttps://github.com/resources/articles
Customer storieshttps://github.com/customer-stories
Events & webinarshttps://github.com/resources/events
Ebooks & reportshttps://github.com/resources/whitepapers
Business insightshttps://github.com/solutions/executive-insights
GitHub Skillshttps://skills.github.com
Documentationhttps://docs.github.com
Customer supporthttps://support.github.com
Community forumhttps://github.com/orgs/community/discussions
Trust centerhttps://github.com/trust-center
Partnershttps://github.com/partners
GitHub SponsorsFund open source developershttps://github.com/sponsors
Security Labhttps://securitylab.github.com
Maintainer Communityhttps://maintainers.github.com
Acceleratorhttps://github.com/accelerator
Archive Programhttps://archiveprogram.github.com
Topicshttps://github.com/topics
Trendinghttps://github.com/trending
Collectionshttps://github.com/collections
Enterprise platformAI-powered developer platformhttps://github.com/enterprise
GitHub Advanced SecurityEnterprise-grade security featureshttps://github.com/security/advanced-security
Copilot for BusinessEnterprise-grade AI featureshttps://github.com/features/copilot/copilot-business
Premium SupportEnterprise-grade 24/7 supporthttps://github.com/premium-support
Pricinghttps://github.com/pricing
Search syntax tipshttps://docs.github.com/search-github/github-code-search/understanding-github-code-search-syntax
documentationhttps://docs.github.com/search-github/github-code-search/understanding-github-code-search-syntax
Sign in https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fnodejs%2Fnode%2Fissues%2F61304
Sign up https://github.com/signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F%3Cuser-name%3E%2F%3Crepo-name%3E%2Fvoltron%2Fissues_fragments%2Fissue_layout&source=header-repo&source_repo=nodejs%2Fnode
Reloadhttps://github.com/nodejs/node/issues/61304
Reloadhttps://github.com/nodejs/node/issues/61304
Reloadhttps://github.com/nodejs/node/issues/61304
nodejs https://github.com/nodejs
nodehttps://github.com/nodejs/node
Please reload this pagehttps://github.com/nodejs/node/issues/61304
Notifications https://github.com/login?return_to=%2Fnodejs%2Fnode
Fork 34.3k https://github.com/login?return_to=%2Fnodejs%2Fnode
Star 115k https://github.com/login?return_to=%2Fnodejs%2Fnode
Code https://github.com/nodejs/node
Issues 1.7k https://github.com/nodejs/node/issues
Pull requests 688 https://github.com/nodejs/node/pulls
Actions https://github.com/nodejs/node/actions
Projects 3 https://github.com/nodejs/node/projects
Security Uh oh! There was an error while loading. Please reload this page. https://github.com/nodejs/node/security
Please reload this pagehttps://github.com/nodejs/node/issues/61304
Insights https://github.com/nodejs/node/pulse
Code https://github.com/nodejs/node
Issues https://github.com/nodejs/node/issues
Pull requests https://github.com/nodejs/node/pulls
Actions https://github.com/nodejs/node/actions
Projects https://github.com/nodejs/node/projects
Security https://github.com/nodejs/node/security
Insights https://github.com/nodejs/node/pulse
New issuehttps://github.com/login?return_to=https://github.com/nodejs/node/issues/61304
New issuehttps://github.com/login?return_to=https://github.com/nodejs/node/issues/61304
Assertion failure crash in TLSWrap::DoWrite with zombie HTTP/2 session (close event not propagated from OS-level CLOSED socket)https://github.com/nodejs/node/issues/61304#top
https://github.com/dglittle
https://github.com/dglittle
dglittlehttps://github.com/dglittle
on Jan 7, 2026https://github.com/nodejs/node/issues/61304#issue-3787716236
Node.js HTTP/2 segfault if underlying socket is unexpectedly closed #49307https://github.com/nodejs/node/issues/49307
Fix a segfault by ensuring TLS Sockets are closed if the underlying stream closes #49327https://github.com/nodejs/node/pull/49327
TLS assertion error in DoWrite #30896https://github.com/nodejs/node/issues/30896
PR #18987https://github.com/nodejs/node/pull/18987
https://github.com
Termshttps://docs.github.com/site-policy/github-terms/github-terms-of-service
Privacyhttps://docs.github.com/site-policy/privacy-policies/github-privacy-statement
Securityhttps://github.com/security
Statushttps://www.githubstatus.com/
Communityhttps://github.community/
Docshttps://docs.github.com/
Contacthttps://support.github.com?tags=dotcom-footer

Viewport: width=device-width


URLs of crawlers that visited me.