Skip to content

Commit b3038b4

Browse files
committed
Fix race condition in RunLoopEventHandler
1 parent 8eed04f commit b3038b4

File tree

2 files changed

+28
-17
lines changed

2 files changed

+28
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ state is to list breaking changes.
2323

2424
### Fixed
2525

26+
- Fixed a race condition in the VST3 GUI event loop on Linux. This could cause
27+
panics with certain versions of Carla.
2628
- The CPAL backend now correctly handles situations where it receives fewer
2729
samples than configured.
2830
- Fixed the handling of multichannel audio in the CPAL backend.

src/wrapper/vst3/view.rs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,8 @@ impl<P: Vst3Plugin> RunLoopEventHandler<P> {
225225
self.tasks.push(task)?;
226226

227227
// We need to use a Unix domain socket to let the host know to call our event handler. In
228-
// theory eventfd would be more suitable here, but Ardour does not support that.
229-
// XXX: This can technically lead to a race condition if the host is currently calling
230-
// `on_fd_is_set()` on another thread and the task has already been popped and executed
231-
// and this value has not yet been written to the socket. Doing it the other way around
232-
// gets you the other situation where the event handler could be run without the task
233-
// being posted yet. In practice this won't cause any issues however.
228+
// theory eventfd would be more suitable here, but Ardour does not support that. This is
229+
// read again in `Self::on_fd_is_set()`.
234230
let notify_value = 1i8;
235231
const NOTIFY_VALUE_SIZE: usize = std::mem::size_of::<i8>();
236232
assert_eq!(
@@ -472,21 +468,34 @@ impl<P: Vst3Plugin> IPlugViewContentScaleSupport for WrapperView<P> {
472468
#[cfg(target_os = "linux")]
473469
impl<P: Vst3Plugin> IEventHandler for RunLoopEventHandler<P> {
474470
unsafe fn on_fd_is_set(&self, _fd: FileDescriptor) {
471+
// There should be a one-to-one correlation to bytes being written to `self.socket_read_fd`
472+
// and events being pushed to `self.tasks`, but because the process of pushing a task and
473+
// notifying this thread through the socket is not atomic we can't reliably just read a byte
474+
// from this socket for every task we process. For instance, if `Self::post_task()` gets
475+
// called while this loop is already running, it could happen that we pop and execute the
476+
// task before the byte gets written to the socket. To avoid this, we'll clear the socket
477+
// upfront, and then execute the tasks afterwards. If this situation does happen, then the
478+
// worst thing that can happen is that this function is called a second time while
479+
// `self.tasks()` is already empty.
480+
let mut notify_value = [0; 32];
481+
loop {
482+
let read_result = libc::read(
483+
self.socket_read_fd,
484+
&mut notify_value as *mut _ as *mut c_void,
485+
std::mem::size_of_val(&notify_value),
486+
);
487+
488+
// If after the first loop the socket contains no more data, then the `read()` call will
489+
// return -1 and `errno` will have been set to `EAGAIN`
490+
if read_result <= 0 {
491+
break;
492+
}
493+
}
494+
475495
// This gets called from the host's UI thread because we wrote some bytes to the Unix domain
476496
// socket. We'll read that data from the socket again just to make REAPER happy.
477497
while let Some(task) = self.tasks.pop() {
478498
self.inner.execute(task, true);
479-
480-
let mut notify_value = 1i8;
481-
const NOTIFY_VALUE_SIZE: usize = std::mem::size_of::<i8>();
482-
assert_eq!(
483-
libc::read(
484-
self.socket_read_fd,
485-
&mut notify_value as *mut _ as *mut c_void,
486-
NOTIFY_VALUE_SIZE
487-
),
488-
NOTIFY_VALUE_SIZE as isize
489-
);
490499
}
491500
}
492501
}

0 commit comments

Comments
 (0)