Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion livekit-rtc/livekit/rtc/_ffi_client.py
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ def disposed(self) -> bool:
def dispose(self) -> None:
if self.handle != INVALID_HANDLE and not self._disposed:
self._disposed = True
assert FfiClient.instance._ffi_lib.livekit_ffi_drop_handle(ctypes.c_uint64(self.handle))
dropped = FfiClient.instance._ffi_lib.livekit_ffi_drop_handle(
ctypes.c_uint64(self.handle)
)
assert dropped
Comment on lines 83 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Fork-safety guard missing on native handle cleanup, allowing child processes to hang or crash on exit

Inherited native handles are released into the dead runtime (livekit_ffi_drop_handle at livekit-rtc/livekit/rtc/_ffi_client.py:86) without the same process-ID check added to requests, so forked child processes can hang or abort during garbage collection.

Impact: A forked child process may hang or crash at exit when Python's garbage collector cleans up inherited handle objects.

Mechanism: FfiHandle.dispose() bypasses the fork guard added to request()

The PR adds a PID check in request() (livekit-rtc/livekit/rtc/_ffi_client.py:274) and in the atexit handler (livekit-rtc/livekit/rtc/_ffi_client.py:265) to prevent calling into the native FFI library from a forked child process. However, FfiHandle.dispose() at livekit-rtc/livekit/rtc/_ffi_client.py:83-87 also calls into the native library via livekit_ffi_drop_handle, and has no such guard.

When a parent process creates livekit objects (Room, tracks, audio streams, etc.), each creates FfiHandle instances. After fork(), the child inherits these objects. When the child exits, Python's GC invokes __del__ (livekit-rtc/livekit/rtc/_ffi_client.py:76-77) on these inherited handles, which calls dispose(), which calls livekit_ffi_drop_handle on the dead native runtime — the exact scenario the PR is designed to prevent.

Multiple callers use FfiHandle extensively: room.py:550, audio_stream.py:138, video_stream.py:70, track.py:35, participant.py:134, and many others.

Suggested change
def dispose(self) -> None:
if self.handle != INVALID_HANDLE and not self._disposed:
self._disposed = True
assert FfiClient.instance._ffi_lib.livekit_ffi_drop_handle(ctypes.c_uint64(self.handle))
dropped = FfiClient.instance._ffi_lib.livekit_ffi_drop_handle(ctypes.c_uint64(self.handle))
assert dropped
def dispose(self) -> None:
if self.handle != INVALID_HANDLE and not self._disposed:
self._disposed = True
if FfiClient.instance._pid != os.getpid():
return # native runtime does not survive fork()
dropped = FfiClient.instance._ffi_lib.livekit_ffi_drop_handle(ctypes.c_uint64(self.handle))
assert dropped
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


def __repr__(self) -> str:
return f"FfiHandle({self.handle})"
Expand Down Expand Up @@ -218,6 +221,7 @@ def instance(cls) -> "FfiClient":
return cls._instance

def __init__(self) -> None:
self._pid = os.getpid()
self._lock = threading.RLock()
self._queue = FfiQueue[proto_ffi.FfiEvent]()

Expand Down Expand Up @@ -253,16 +257,25 @@ def __init__(self) -> None:
)

ffi_lib = self._ffi_lib
init_pid = self._pid

@atexit.register
def _dispose_lk_ffi() -> None:
if os.getpid() != init_pid:
return
ffi_lib.livekit_ffi_dispose()

@property
def queue(self) -> FfiQueue[proto_ffi.FfiEvent]:
return self._queue

def request(self, req: proto_ffi.FfiRequest) -> proto_ffi.FfiResponse:
if self._pid != os.getpid():
raise RuntimeError(
"livekit.rtc was used in a parent process before fork(); the native "
"runtime cannot be used across fork(). Do not create or connect a Room "
"(or use any livekit.rtc object) before forking child processes."
)
proto_data = req.SerializeToString()
proto_len = len(proto_data)
data = (ctypes.c_ubyte * proto_len)(*proto_data)
Expand Down
Loading