Skip to content

Conversation

@arturgawlik
Copy link
Contributor

@arturgawlik arturgawlik commented Oct 28, 2025

Fixes #60456 by adding new finish event listener after socket.destroySoon call only if they were not added previously on given socket.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/net

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. net Issues and PRs related to the net subsystem. labels Oct 28, 2025
@arturgawlik arturgawlik force-pushed the do-not-leak-when-destroying-socket-multiple-times branch from 524d5c5 to 8473258 Compare October 28, 2025 21:28
@arturgawlik arturgawlik force-pushed the do-not-leak-when-destroying-socket-multiple-times branch from 8473258 to 6e33069 Compare October 28, 2025 21:32
@arturgawlik arturgawlik marked this pull request as ready for review October 28, 2025 22:04
@codecov
Copy link

codecov bot commented Oct 28, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.54%. Comparing base (fb34515) to head (c9a2c5e).
⚠️ Report is 46 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #60469      +/-   ##
==========================================
- Coverage   88.58%   88.54%   -0.05%     
==========================================
  Files         704      704              
  Lines      207826   207886      +60     
  Branches    40054    40050       -4     
==========================================
- Hits       184106   184071      -35     
- Misses      15761    15844      +83     
- Partials     7959     7971      +12     
Files with missing lines Coverage Δ
lib/net.js 95.29% <100.00%> (+0.01%) ⬆️

... and 61 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

lib/net.js Outdated
Socket.prototype._final = function(cb) {
// If still connecting - defer handling `_final` until 'connect' will happen
if (this.connecting) {
if (this.connecting && !this._finalizingOnConnect) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this guard is need, wriable._final() is only called once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, maybe I'm missing something here 🤔 but from what I have been debugged it seems that Socket._final() is called for every socket.destroySoon() call in described scenario in the linked issue #60456

P.S. I've updated implementation to return from this function even if _finalizingOnConnect is already true (as it was previously).

Copy link
Member

Choose a reason for hiding this comment

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

const net = require('net');

const socket = net.createConnection({
  host: 'example.com',
  port: 80,
  lookup() {}
});

socket.destroySoon();
socket.destroySoon();
socket.destroySoon();
socket.destroySoon();
socket.destroySoon();

console.log(socket.listenerCount('connect')); // Prints 1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. My test didn't reproduce mentioned issue well. I reworked it to do it better. So now it looks like this:

const net = require('net');

const socket = new net.Socket();
socket.on('error', () => {
  // noop
});
const connectOptions = { host: "example.com", port: 1234 };

socket.connect(connectOptions);
socket.destroySoon(); // Adds "connect" and "finish" event listeners when socket has "writable" state
socket.destroy(); // Makes imideditly socket again "writable"

socket.connect(connectOptions);
socket.destroySoon(); // Should not duplicate "connect" and "finish" event listeners

console.log(socket.listeners('finish').length); // Prints 2
console.log(socket.listeners('connect').length); // Prints also 2

and with it we can observe that second call to socket.destroySoon() adds additional event listeners for finish and connect events, which could lead to memory leak.

socket.destroySoon(); // Adds "connect" and "finish" event listeners when socket has "writable" state
socket.destroy(); // Makes imideditly socket again "writable"

socket.connect(connectOptions);
Copy link
Member

@lpinca lpinca Oct 30, 2025

Choose a reason for hiding this comment

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

This is what actually makes the socket writable again as it calls socket._undestroy(). It is not recommended to call socket.connect() on a previously destroyed socket as its state might not be well defined. The issue you are describing can also happen in other cases with this usage pattern, for example:

const net = require('net');

const options = {
  host: 'example.com',
  port: 80,
  lookup() {}
};

const socket = new net.Socket();

socket.connect(options);
socket.read();
socket.destroy();

socket.connect(options);
socket.read();
socket.destroy();

socket.connect(options);
socket.read();
socket.destroy();

console.log(socket.listenerCount('connect')); // Prints 3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank You for Your review! But do You think that mentioned issue should be even addressed? If yes I will analyze it deeper to provide better test that is reproducing this issue.

Copy link
Member

@lpinca lpinca Oct 31, 2025

Choose a reason for hiding this comment

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

I think we should fix the issue where multiple 'finish' events are added if socket.destroySoon() is called multiple times on the same socket. A possible fix is to make socket.destroySoon() a noop after the first call.

Copy link
Contributor Author

@arturgawlik arturgawlik Nov 1, 2025

Choose a reason for hiding this comment

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

I've removed changes for connect event listener, and leaved for finish (in destroySoon). I didn't make socket.destroySoon() completely noop after first call, because I still want this function to work back as it should when finish event eventually is emitted, and calling socket.destroySoon() potentially becomes back legitimate thing. But please let me know if You meant something else by writing "a noop after the first call".

@arturgawlik arturgawlik marked this pull request as draft November 1, 2025 00:10
@arturgawlik arturgawlik marked this pull request as ready for review November 1, 2025 22:48
arturgawlik and others added 2 commits November 2, 2025 23:34
…eners.js

Co-authored-by: Luigi Pinca <luigipinca@gmail.com>
…eners.js

Co-authored-by: Luigi Pinca <luigipinca@gmail.com>
lib/net.js Outdated
else
this.once('finish', this.destroy);
else {
this.destroyingOnFinish = true;
Copy link
Member

Choose a reason for hiding this comment

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

please use a Symbol for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right 👍 Symbol used ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. net Issues and PRs related to the net subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

socket.destroySoon causes two EventEmitter memory leaks when repetitively using it on a socket that is trying to connect

4 participants