Skip to content

Commit b331de5

Browse files
committed
fix(stream): keep spa_hook in unmanaged memory across GC moves
pw_stream_add_listener stores the spa_hook pointer in the stream's listener list. It was a managed instance field held only by a GCHandleType.Normal (non-pinning) handle, so a GC heap compaction moved the object and left PipeWire dereferencing a stale hook - a hard crash in spa_list_remove when the stream next emitted an event (param_changed on consumer link, and on teardown). Allocate the hook in unmanaged memory so its address is stable for the stream's lifetime; free it after the stream is destroyed.
1 parent 416615e commit b331de5

1 file changed

Lines changed: 16 additions & 4 deletions

File tree

src/PipeWire.NET/PipeWireStreamCore.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ internal sealed unsafe class PipeWireStreamCore : IAsyncDisposable
7979

8080
private pw_stream* _stream;
8181
private pw_stream_events* _events;
82-
private spa_hook _hook;
82+
// The spa_hook MUST live in unmanaged memory, not as a managed field: pw_stream_add_listener stores this
83+
// pointer in the stream's listener list, and the GC compacting the heap would move a managed field, leaving
84+
// PipeWire with a dangling pointer that crashes (spa_list_remove on freed memory) the next time it emits an
85+
// event. _selfHandle is GCHandleType.Normal (non-pinning), so it does not keep a field address stable.
86+
private spa_hook* _hook;
8387
private GCHandle _selfHandle;
8488
private volatile bool _disposed;
8589

@@ -116,6 +120,7 @@ internal PipeWireStreamCore(
116120

117121
_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);
118122

123+
_hook = (spa_hook*)NativeMemory.AllocZeroed((nuint)sizeof(spa_hook));
119124
_events = (pw_stream_events*)NativeMemory.AllocZeroed((nuint)sizeof(pw_stream_events));
120125
_events->version = Native.PW_VERSION_STREAM_EVENTS;
121126
_events->process = &OnProcess;
@@ -137,12 +142,13 @@ internal PipeWireStreamCore(
137142
_selfHandle.Free();
138143
NativeMemory.Free(_events);
139144
_events = null;
145+
NativeMemory.Free(_hook);
146+
_hook = null;
140147
throw new InvalidOperationException("pw_stream_new failed.");
141148
}
142149

143-
fixed (spa_hook* hookPtr = &_hook)
144-
Native.pw_stream_add_listener(_stream, hookPtr, _events,
145-
(void*)GCHandle.ToIntPtr(_selfHandle));
150+
Native.pw_stream_add_listener(_stream, _hook, _events,
151+
(void*)GCHandle.ToIntPtr(_selfHandle));
146152
}
147153
}
148154

@@ -206,6 +212,12 @@ public ValueTask DisposeAsync()
206212
NativeMemory.Free(_events);
207213
_events = null;
208214
}
215+
// Free the hook only after the stream is destroyed (destroy removes the listener that points at it).
216+
if (_hook is not null)
217+
{
218+
NativeMemory.Free(_hook);
219+
_hook = null;
220+
}
209221
if (_selfHandle.IsAllocated) _selfHandle.Free();
210222

211223
return ValueTask.CompletedTask;

0 commit comments

Comments
 (0)