Skip to content

fix: remove constructor.Reset() from S7Client/S7Server destructors - #107

Open
JordiB-Inserma wants to merge 2 commits into
mathiask88:masterfrom
JordiB-Inserma:fix/s7-driver-linux-segfault
Open

fix: remove constructor.Reset() from S7Client/S7Server destructors#107
JordiB-Inserma wants to merge 2 commits into
mathiask88:masterfrom
JordiB-Inserma:fix/s7-driver-linux-segfault

Conversation

@JordiB-Inserma

Copy link
Copy Markdown

Summary

S7Client and S7Server destructors were calling constructor.Reset() on a static Nan::Persistent<FunctionTemplate>, which destroys the V8 constructor template shared across all instances. This causes segfaults on Linux when applications create and destroy S7Client instances repeatedly (e.g. SCADA/HMI reconnection loops like FUXA).

Root cause

constructor is a static class member initialized once during module load (Init). It stores the FunctionTemplate that V8 uses to create new instances. Both ~S7Client() and ~S7Server() were calling constructor.Reset(), which releases the persistent handle to the static FunctionTemplate.

Since this is a static resource shared across ALL instances:

  1. new snap7.S7Client() — works, FunctionTemplate alive
  2. Instance destroyed (GC/reference lost) → constructor.Reset() destroys the static template
  3. Next new snap7.S7Client() → V8's internal constructor resolution fails → SEGFAULT

The crash is non-deterministic and depends on V8 GC timing. On Linux, the GC is more aggressive, making this crash frequent in production SCADA systems.

Fix

Remove constructor.Reset() from both ~S7Client() (node_snap7_client.cpp:722) and ~S7Server() (node_snap7_server.cpp:682). The static FunctionTemplate should live for the lifetime of the module, not per-instance.

Changes

File Change
src/node_snap7_client.cpp Removed constructor.Reset() from ~S7Client()
src/node_snap7_server.cpp Removed constructor.Reset() from ~S7Server()

Stack trace before fix (Linux)

Thread 1 "FUXA" received signal SIGSEGV, Segmentation fault.
#0  v8::internal::ApiNatives::InstantiateFunction(...)
#1  v8::FunctionTemplate::GetFunction(v8::Local<v8::Context>)
#2  node_snap7::S7Client::New(Nan::FunctionCallbackInfo<v8::Value> const&)

Related

  • Upstream pthread fix: davenardella/snap7#29 — prerequisite for full Linux stability (fixes undefined behavior when joining detached threads)
  • Upstream issues: davenardella/snap7#19, #23
  • Environment: Linux, Node.js 18+, node-snap7 1.0.9, FUXA SCADA v1.3.3

S7Client::~S7Client() and S7Server::~S7Server() were calling
constructor.Reset() on a static Nan::Persistent<FunctionTemplate>.
This releases the persistent handle to the FunctionTemplate that
V8 uses to create new instances of the object.

Since constructor is a static class member shared across ALL
instances, destroying ANY single instance invalidates the
constructor template for every existing and future instance.
When V8's GC collects the invalidated template and a new
instance is created, S7Client::New tries to resolve the
constructor via FunctionTemplate::GetFunction() which returns
a stale/collected handle, causing a segfault.

This crash is non-deterministic and depends on V8 GC timing.
On Linux, the GC is more aggressive, making the crash frequent
in applications with repeated create/destroy cycles (e.g.
SCADA/HMI reconnection loops).

The fix is simply removing constructor.Reset() from both
destructors. The static FunctionTemplate should live for the
lifetime of the module, not per-instance.

References:
- davenardella/snap7#29 (upstream pthread fix, prerequisite
  for full Linux stability)
- davenardella/snap7#19 (server destructor race)
Add AGENTS.md, .opencode/, .claude/, .cursorrules, and
.github/copilot-instructions.md to both .gitignore and
.npmignore to keep AI assistant configuration files out of
version control and npm package distribution.
JordiB-Inserma added a commit to JordiB-Inserma/FUXA that referenced this pull request Jul 28, 2026
Move datatypes initialization from create() into the S7client
constructor, using the real s7client instance that is retained
for the connection lifetime instead of creating a throwaway
S7Client solely to read S7WL* constants.

The throwaway instance was garbage collected immediately, and its
destructor called constructor.Reset() on the shared
Nan::Persistent<FunctionTemplate> in node-snap7, corrupting the
prototype for all subsequent instances — causing segfaults on
Linux (mathiask88/node-snap7#107).

The guard if (!datatypes) ensures initialization runs only once.
The S7WL* constants are prototype properties (not statics on the
constructor), so an instance is required to read them — but the
real instance is retained in the closure, avoiding the destructor
path entirely.

Supersedes d876199 (add missing new keyword) which corrected
syntax but did not address the lifecycle issue.
unocelli pushed a commit to frangoteam/FUXA that referenced this pull request Jul 30, 2026
…2437)

* fix(s7): add missing 'new' keyword in S7Client instantiation

The datatypes initialization in create() called snap7.S7Client()
without the 'new' keyword, creating a throwaway instance that would
be garbage-collected immediately. On Linux, the GC triggers the
destructor which calls constructor.Reset() on the static
Nan::Persistent<FunctionTemplate>, corrupting the shared prototype
for all subsequent instances — causing a segfault.

This is the application-side counterpart of the fix applied in
mathiask88/node-snap7#107 (constructor.Reset() removal in
~S7Client/~S7Server destructors).

Upstream references:
- mathiask88/node-snap7#107: remove constructor.Reset() in destructors
- davenardella/snap7#29: fix POSIX thread joinable lifecycle

* fix(s7): properly reject promises on read/write errors

_readVars, _writeDB, and _writeVars had a critical bug where
callback errors were handled with 'return _getErr(err)' instead of
'return reject(_getErr(err))'. Since _getErr only formats the error
string, the Promise was left in a permanently pending state — the
resolve/reject callbacks were never called.

This caused:
- working flag stuck at true, blocking all subsequent operations
- overloading counter to accumulate without reset
- forced device reconnection (in _checkWorking) to recover
- amplified create/destroy cycles that trigger constructor.Reset()
  segfaults on Linux (see mathiask88/node-snap7#107)

Applied to three functions:
- _readVars: ReadMultiVars callback
- _writeDB: DBWrite callback
- _writeVars: WriteMultiVars callback

* fix(s7): remove forced disconnect on polling overload

When _checkWorking detected repeated overloads (working=true while a
new poll arrived), it called s7client.Disconnect() after 3
consecutive overloads. This forced disconnection triggered the
device's cleanup/reconnect cycle, which creates and destroys S7Client
instances — amplifying the constructor.Reset() segfault on Linux
(mathiask88/node-snap7#107).

The overload is a normal condition when polling intervals are shorter
than PLC response times. Instead of disconnecting, the poll is now
simply skipped (returns false), allowing the current operation to
complete naturally. The overloading counter still increments and logs
a warning for diagnostics.

* fix(s7): lazy init datatypes from real S7Client instance

Move datatypes initialization from create() into the S7client
constructor, using the real s7client instance that is retained
for the connection lifetime instead of creating a throwaway
S7Client solely to read S7WL* constants.

The throwaway instance was garbage collected immediately, and its
destructor called constructor.Reset() on the shared
Nan::Persistent<FunctionTemplate> in node-snap7, corrupting the
prototype for all subsequent instances — causing segfaults on
Linux (mathiask88/node-snap7#107).

The guard if (!datatypes) ensures initialization runs only once.
The S7WL* constants are prototype properties (not statics on the
constructor), so an instance is required to read them — but the
real instance is retained in the closure, avoiding the destructor
path entirely.

Supersedes d876199 (add missing new keyword) which corrected
syntax but did not address the lifecycle issue.
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