libplctag wrapper crashes the process: callback on a garbage-collected delegate (callback_func_ex) #578

Closed
opened 2026-10-07 19:40:43 -04:00 by dohertj2 · 2 comments
Owner

Found 2026-10-07 during the native AB transport real-press read-only gate (Task 21, branch feat/native-ab-transport, plan docs/plans/2026-10-07-native-ab-transport.md).

What happened

At the end of a read-only differential/latency run against a production CompactLogix 5069-L320ERS2 (Z-35162), the dotnet test host process died with:

Process terminated.
A callback was made on a garbage collected delegate of type
'libplctag.NativeImport!libplctag.NativeImport.plctag+callback_func_ex::Invoke'.

This is a fatal CLR failure (not catchable). The packet capture shows the last session never sent a Forward Close, so the process died before or during cleanup. No writes were involved (capture verified: zero write services).

Why it matters

The libplctag .NET wrapper (libplctag 1.5.2) registers a native callback. If the managed delegate is collected while the native library can still invoke it — e.g. a Tag finalized or disposed while an async read is in flight — the whole process terminates. If the production AbCip/AbLegacy libplctag adapters (Libplctag*.cs) can reach the same state (driver shutdown/reinit with reads pending, a timeout-cancelled ReadAsync), an OtOpcUa server node would crash outright.

To do

  • Root-cause: which handle/delegate is collected (harness-only, or reachable from the production adapters).
  • Production exposure verdict with a reproduction against ab_server (dispose/finalize a tag with a read pending).
  • Fix in the adapters if exposed, with a pinning test; otherwise fix the harness.
  • Note: switching a driver to Transport=Native removes the libplctag native callback path entirely for that driver.

Investigation in progress on the branch; this issue will be updated with the verdict.

Found 2026-10-07 during the native AB transport real-press read-only gate (Task 21, branch `feat/native-ab-transport`, plan `docs/plans/2026-10-07-native-ab-transport.md`). ## What happened At the end of a read-only differential/latency run against a production CompactLogix 5069-L320ERS2 (Z-35162), the `dotnet test` host process died with: ``` Process terminated. A callback was made on a garbage collected delegate of type 'libplctag.NativeImport!libplctag.NativeImport.plctag+callback_func_ex::Invoke'. ``` This is a fatal CLR failure (not catchable). The packet capture shows the last session never sent a Forward Close, so the process died before or during cleanup. No writes were involved (capture verified: zero write services). ## Why it matters The libplctag .NET wrapper (`libplctag` 1.5.2) registers a native callback. If the managed delegate is collected while the native library can still invoke it — e.g. a `Tag` finalized or disposed while an async read is in flight — the **whole process** terminates. If the production AbCip/AbLegacy libplctag adapters (`Libplctag*.cs`) can reach the same state (driver shutdown/reinit with reads pending, a timeout-cancelled `ReadAsync`), an OtOpcUa server node would crash outright. ## To do - [ ] Root-cause: which handle/delegate is collected (harness-only, or reachable from the production adapters). - [ ] Production exposure verdict with a reproduction against ab_server (dispose/finalize a tag with a read pending). - [ ] Fix in the adapters if exposed, with a pinning test; otherwise fix the harness. - [ ] Note: switching a driver to `Transport=Native` removes the libplctag native callback path entirely for that driver. Investigation in progress on the branch; this issue will be updated with the verdict.
dohertj2 added the driver/abcipdriver/ablegacy labels 2026-10-07 19:40:43 -04:00
Author
Owner

Verdict: production-reachable — fixed on feat/native-ab-transport in 6cd68762 (under review).

Root cause (libplctag.NET 1.5.2): when a tag's native create fails (e.g. NotFound for a mistyped tag name), the wrapper leaves the native tag and its registered callback in place, and Tag.Dispose() does not release them. Once the GC collects the managed delegate, libplctag's process-exit shutdown invokes it and the CLR terminates the process.

Reproduced against ab_server with the production LibplctagTagRuntime: without the fix the process exits 134 with the exact callback_func_ex message; with it, exit 0.

Exposure: every libplctag create path in AbCip and AbLegacy — data path, probe loop, Test Connect, symbol browse and template reads. Before the fix:

  • a single authored tag name that doesn't exist on the controller crashes the server node at stop/restart;
  • an unreachable device leaks one reconnecting native tag per failed create.

Fix: after a failed initialisation the adapters destroy the leftover native tag; tests fail with the fix removed. It relies on two private field names in the 1.5.2 wrapper; a test pins them, and if they change the code keeps the tag alive (small leak, no crash) rather than crashing.

Drivers set to Transport=Native don't use the libplctag callback path at all.

**Verdict: production-reachable — fixed on `feat/native-ab-transport` in `6cd68762` (under review).** Root cause (libplctag.NET 1.5.2): when a tag's native create fails (e.g. `NotFound` for a mistyped tag name), the wrapper leaves the native tag and its registered callback in place, and `Tag.Dispose()` does not release them. Once the GC collects the managed delegate, libplctag's process-exit shutdown invokes it and the CLR terminates the process. Reproduced against ab_server with the **production** `LibplctagTagRuntime`: without the fix the process exits 134 with the exact `callback_func_ex` message; with it, exit 0. Exposure: every libplctag create path in AbCip and AbLegacy — data path, probe loop, Test Connect, symbol browse and template reads. Before the fix: - a single authored tag name that doesn't exist on the controller crashes the server node at stop/restart; - an unreachable device leaks one reconnecting native tag per failed create. Fix: after a failed initialisation the adapters destroy the leftover native tag; tests fail with the fix removed. It relies on two private field names in the 1.5.2 wrapper; a test pins them, and if they change the code keeps the tag alive (small leak, no crash) rather than crashing. Drivers set to `Transport=Native` don't use the libplctag callback path at all.
Author
Owner

Fixed before this sweep (6cd68762, merged in 04b65632): adapters release the orphaned native tag after a failed create; the release helper is now shared by both drivers (#580). (merge 04b65632 on master, pushed 2026-10-08)

Fixed before this sweep (6cd68762, merged in 04b65632): adapters release the orphaned native tag after a failed create; the release helper is now shared by both drivers (#580). (merge `04b65632` on master, pushed 2026-10-08)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#578