Temporary Scalars can be finalized while native code still uses them (crash in Tensor.add under concurrency)
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 45/100
Direzione di ricerca
Start with the implicit Scalar conversions and native call in src/TorchSharp/Tensor/Tensor.Math.cs, then review the referenced issues #1434 and #1496 for existing approaches. Check other examples listed in the report, including softplus and scalar operators, to understand the scope. Done means every Scalar passed to native code stays alive until the call returns; the issue does not specify a deterministic reproduction test.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
A temporary Scalar that TorchSharp creates for a native call can be freed by its finalizer while libtorch is still reading it. That is a use-after-free: it can crash the process or give wrong results.
Scalar frees its native value in a finalizer, isn't a SafeHandle, and isn't tracked by any DisposeScope. Many methods create one implicitly and pass only its handle to native code, e.g. src/TorchSharp/Tensor/Tensor.Math.cs:
public Tensor add(Tensor target)
{
return add(target, 1); // temporary `Scalar` for alpha
}
public Tensor add(Tensor target, `Scalar` alpha)
{
var res = THSTensor_add(Handle, target.Handle, alpha.Handle); // nothing keeps alpha alive from here on
...
}
Once .Handle has been read, the JIT may consider the Scalar unreachable. A garbage collection that starts during the native call (for example one triggered by another thread's allocations) can then run ~Scalar() → THSTorch_dispose_scalar mid-call.
The same pattern is everywhere a .NET number is implicitly converted to a Scalar argument:
- add/sub with their default alpha, and tensor +;
- scalar arithmetic operators;
- masked_fill(mask, 0), pow(2), eq(0);
- functions like softplus, which convert their double parameters internally.
#1434 and #1496 already dispose these temporaries, framed as a memory leak. They would also fix this crash, because a Scalar that is disposed after the call stays alive during it. This issue is to report that the problem is a memory-safety bug as well as a leak, which may help prioritize those PRs.
To Reproduce
The failure is intermittent and depends on GC timing, so there's no deterministic repro. We hit it in StanzaSharp, which runs Stanza's NLP models on TorchSharp. In a CI test, 8 threads run inference concurrently on shared modules, each thread inside its own DisposeScope and no_grad(). The failing line was:
var predicted = 1 + torch.nn.functional.softplus(dist);
Crash (process terminated):
System.Runtime.InteropServices.ExternalException (0x80004005): Expected false to be true, but got false.
Exception raised from toLong at C:\a\pytorch\pytorch\pytorch\c10/core/Scalar.h:140 (most recent call first):
c10.dll!c10::detail::torchCheckFail
torch_cpu.DLL!at::native::add
torch_cpu.DLL!at::Tensor::add
at TorchSharp.torch.CheckForErrors()
at TorchSharp.torch.Tensor.op_Addition(Int32 left, Tensor right)
...
It crashed once on Windows Arm64 and passed on a rerun. It never reproduced on x64, even with a thread looping GC.Collect(); GC.WaitForPendingFinalizers(); during inference. Presumably the x64 JIT happens to keep the temporary alive across the call, but nothing guarantees it.
Expected behavior
Every Scalar passed to native code stays alive until the call returns, whatever the GC does.
Please complete the following information:
- OS: Windows 11 on Arm64 (GitHub Actions windows-11-arm runner), .NET 10
- Package Type: TorchSharp 0.107.0 with libtorch-cpu-win-arm64 2.10.0
- Version: 0.107.0
Additional context
Possible fixes, any of:
- merge #1434 / #1496 (dispose temporaries right after the call);
- add
GC.KeepAlive(scalar)after each P/Invoke that takes aScalarhandle; - make
Scalarwrap a SafeHandle, which keeps it alive for the duration of every P/Invoke and fixes all call sites at once.
Tensor temporaries created outside a DisposeScope probably have the same exposure.
Our workaround: never let TorchSharp create a Scalar. We keep our own in static readonly fields or using var locals, and call the overloads that take them, e.g. a.add(b, one) instead of a + b
- Lingua principale
- C#
- Stelle
- 1.9k
- Fork
- 230
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di dotnet/TorchSharp
-
Difficoltà 4/5 Più di una settimana Idoneità per principianti 30/100
dotnet/TorchSharp#1581 ·
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
dotnet/TorchSharp#1579 ·
-
bug
Difficoltà 4/5 3-5 giorni Idoneità per principianti 56/100
dotnet/TorchSharp#1574 ·
-
bug
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
dotnet/TorchSharp#1567 ·
-
question
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
dotnet/TorchSharp#1557 ·
Tutte le issue di dotnet/TorchSharp
Issue simili
-
Bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 73/100
dotnet/roslynator#1859 ·
I maintainer di solito rispondono entro 1 giorno
-
area:jobads-cv BE P3
Difficoltà 1/5 1-3 ore Idoneità per principianti 74/100
klasolsson81/jobbliggaren#2240 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
k2-fsa/sherpa-onnx#4100 ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno
-
:watch: Not Triaged
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno