Skip to content

Commit 7af245c

Browse files
fix: fs::Scope event deadlocks (#15474)
* fix: `fs::Scope` event deadlocks * Unlisten first and add unit test * Add change file
1 parent 4c8bb98 commit 7af245c

2 files changed

Lines changed: 157 additions & 59 deletions

File tree

.changes/fs-scope-once-deadlock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": "patch:bug"
3+
---
4+
5+
Fix `tauri::scope::fs::Scope::once` deadlocks

crates/tauri/src/scope/fs.rs

Lines changed: 152 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77
fmt,
88
path::{Path, PathBuf, MAIN_SEPARATOR},
99
sync::{
10-
atomic::{AtomicU32, Ordering},
10+
atomic::{AtomicBool, AtomicU32, Ordering},
1111
Arc, Mutex,
1212
},
1313
};
@@ -29,19 +29,34 @@ pub enum Event {
2929

3030
type EventListener = Box<dyn Fn(&Event) + Send>;
3131

32+
enum Pending {
33+
Unlisten(ScopeEventId),
34+
Listen {
35+
id: ScopeEventId,
36+
handler: EventListener,
37+
},
38+
Emit(Event),
39+
}
40+
41+
struct ScopeInner {
42+
allowed_patterns: Mutex<HashSet<Pattern>>,
43+
forbidden_patterns: Mutex<HashSet<Pattern>>,
44+
match_options: glob::MatchOptions,
45+
event_listeners: Mutex<HashMap<ScopeEventId, EventListener>>,
46+
pending: Mutex<Vec<Pending>>,
47+
emitting: AtomicBool,
48+
next_event_id: AtomicU32,
49+
}
50+
3251
/// Scope for filesystem access.
3352
#[derive(Clone)]
3453
pub struct Scope {
35-
allowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
36-
forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
37-
event_listeners: Arc<Mutex<HashMap<ScopeEventId, EventListener>>>,
38-
match_options: glob::MatchOptions,
39-
next_event_id: Arc<AtomicU32>,
54+
inner: Arc<ScopeInner>,
4055
}
4156

4257
impl Scope {
4358
fn next_event_id(&self) -> u32 {
44-
self.next_event_id.fetch_add(1, Ordering::Relaxed)
59+
self.inner.next_event_id.fetch_add(1, Ordering::Relaxed)
4560
}
4661
}
4762

@@ -51,6 +66,7 @@ impl fmt::Debug for Scope {
5166
.field(
5267
"allowed_patterns",
5368
&self
69+
.inner
5470
.allowed_patterns
5571
.lock()
5672
.unwrap()
@@ -61,6 +77,7 @@ impl fmt::Debug for Scope {
6177
.field(
6278
"forbidden_patterns",
6379
&self
80+
.inner
6481
.forbidden_patterns
6582
.lock()
6683
.unwrap()
@@ -208,66 +225,122 @@ impl Scope {
208225
};
209226

210227
Ok(Self {
211-
allowed_patterns: Arc::new(Mutex::new(allowed_patterns)),
212-
forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
213-
event_listeners: Default::default(),
214-
next_event_id: Default::default(),
215-
match_options: glob::MatchOptions {
216-
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
217-
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
218-
require_literal_separator: true,
219-
require_literal_leading_dot,
220-
..Default::default()
221-
},
228+
inner: Arc::new(ScopeInner {
229+
allowed_patterns: Mutex::new(allowed_patterns),
230+
forbidden_patterns: Mutex::new(forbidden_patterns),
231+
event_listeners: Default::default(),
232+
pending: Default::default(),
233+
emitting: AtomicBool::new(false),
234+
next_event_id: Default::default(),
235+
match_options: glob::MatchOptions {
236+
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
237+
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
238+
require_literal_separator: true,
239+
require_literal_leading_dot,
240+
..Default::default()
241+
},
242+
}),
222243
})
223244
}
224245

225246
/// The list of allowed patterns.
226247
pub fn allowed_patterns(&self) -> HashSet<Pattern> {
227-
self.allowed_patterns.lock().unwrap().clone()
248+
self.inner.allowed_patterns.lock().unwrap().clone()
228249
}
229250

230251
/// The list of forbidden patterns.
231252
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
232-
self.forbidden_patterns.lock().unwrap().clone()
253+
self.inner.forbidden_patterns.lock().unwrap().clone()
233254
}
234255

235256
/// Listen to an event on this scope.
236257
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
237258
let id = self.next_event_id();
238-
self.listen_with_id(id, f);
259+
self.listen_with_id(id, Box::new(f));
239260
id
240261
}
241262

242-
fn listen_with_id<F: Fn(&Event) + Send + 'static>(&self, id: ScopeEventId, f: F) {
243-
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
263+
fn listen_with_id(&self, id: ScopeEventId, handler: EventListener) {
264+
if self.inner.emitting.load(Ordering::Relaxed) {
265+
self
266+
.inner
267+
.pending
268+
.lock()
269+
.unwrap()
270+
.push(Pending::Listen { id, handler });
271+
} else {
272+
self
273+
.inner
274+
.event_listeners
275+
.lock()
276+
.unwrap()
277+
.insert(id, handler);
278+
}
244279
}
245280

246281
/// Listen to an event on this scope and immediately unlisten.
247282
pub fn once<F: FnOnce(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
248-
let listerners = self.event_listeners.clone();
283+
let self_ = self.clone();
249284
let handler = std::cell::Cell::new(Some(f));
250285
let id = self.next_event_id();
251-
self.listen_with_id(id, move |event| {
252-
listerners.lock().unwrap().remove(&id);
253-
let handler = handler
254-
.take()
255-
.expect("attempted to call handler more than once");
256-
handler(event)
257-
});
286+
self.listen_with_id(
287+
id,
288+
Box::new(move |event| {
289+
self_.unlisten(id);
290+
let handler = handler
291+
.take()
292+
.expect("attempted to call handler more than once");
293+
handler(event);
294+
}),
295+
);
258296
id
259297
}
260298

261299
/// Removes an event listener on this scope.
262300
pub fn unlisten(&self, id: ScopeEventId) {
263-
self.event_listeners.lock().unwrap().remove(&id);
301+
if self.inner.emitting.load(Ordering::Relaxed) {
302+
self
303+
.inner
304+
.pending
305+
.lock()
306+
.unwrap()
307+
.push(Pending::Unlisten(id));
308+
} else {
309+
self.inner.event_listeners.lock().unwrap().remove(&id);
310+
}
264311
}
265312

266313
fn emit(&self, event: Event) {
267-
let listeners = self.event_listeners.lock().unwrap();
268-
let handlers = listeners.values();
269-
for listener in handlers {
270-
listener(&event);
314+
let was_emitting = self.inner.emitting.swap(true, Ordering::Relaxed);
315+
if was_emitting {
316+
self
317+
.inner
318+
.pending
319+
.lock()
320+
.unwrap()
321+
.push(Pending::Emit(event));
322+
return;
323+
}
324+
325+
{
326+
let listeners = self.inner.event_listeners.lock().unwrap();
327+
let handlers = listeners.values();
328+
for listener in handlers {
329+
listener(&event);
330+
}
331+
self.inner.emitting.store(false, Ordering::Relaxed);
332+
}
333+
334+
let pending = {
335+
let mut lock = self.inner.pending.lock().unwrap();
336+
std::mem::take(&mut *lock)
337+
};
338+
for action in pending {
339+
match action {
340+
Pending::Unlisten(id) => self.unlisten(id),
341+
Pending::Listen { id, handler } => self.listen_with_id(id, handler),
342+
Pending::Emit(event) => self.emit(event),
343+
}
271344
}
272345
}
273346

@@ -278,7 +351,7 @@ impl Scope {
278351
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
279352
let path = path.as_ref();
280353
{
281-
let mut list = self.allowed_patterns.lock().unwrap();
354+
let mut list = self.inner.allowed_patterns.lock().unwrap();
282355

283356
// allow the directory to be read
284357
push_pattern(&mut list, path, escaped_pattern)?;
@@ -297,7 +370,7 @@ impl Scope {
297370
pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
298371
let path = path.as_ref();
299372
push_pattern(
300-
&mut self.allowed_patterns.lock().unwrap(),
373+
&mut self.inner.allowed_patterns.lock().unwrap(),
301374
path,
302375
escaped_pattern,
303376
)?;
@@ -311,7 +384,7 @@ impl Scope {
311384
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
312385
let path = path.as_ref();
313386
{
314-
let mut list = self.forbidden_patterns.lock().unwrap();
387+
let mut list = self.inner.forbidden_patterns.lock().unwrap();
315388

316389
// allow the directory to be read
317390
push_pattern(&mut list, path, escaped_pattern)?;
@@ -330,7 +403,7 @@ impl Scope {
330403
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
331404
let path = path.as_ref();
332405
push_pattern(
333-
&mut self.forbidden_patterns.lock().unwrap(),
406+
&mut self.inner.forbidden_patterns.lock().unwrap(),
334407
path,
335408
escaped_pattern,
336409
)?;
@@ -349,21 +422,23 @@ impl Scope {
349422
if let Ok(path) = path {
350423
let path: PathBuf = path.components().collect();
351424
let forbidden = self
425+
.inner
352426
.forbidden_patterns
353427
.lock()
354428
.unwrap()
355429
.iter()
356-
.any(|p| p.matches_path_with(&path, self.match_options));
430+
.any(|p| p.matches_path_with(&path, self.inner.match_options));
357431

358432
if forbidden {
359433
false
360434
} else {
361435
let allowed = self
436+
.inner
362437
.allowed_patterns
363438
.lock()
364439
.unwrap()
365440
.iter()
366-
.any(|p| p.matches_path_with(&path, self.match_options));
441+
.any(|p| p.matches_path_with(&path, self.inner.match_options));
367442

368443
allowed
369444
}
@@ -381,11 +456,12 @@ impl Scope {
381456
if let Ok(path) = path {
382457
let path: PathBuf = path.components().collect();
383458
self
459+
.inner
384460
.forbidden_patterns
385461
.lock()
386462
.unwrap()
387463
.iter()
388-
.any(|p| p.matches_path_with(&path, self.match_options))
464+
.any(|p| p.matches_path_with(&path, self.inner.match_options))
389465
} else {
390466
true
391467
}
@@ -424,29 +500,35 @@ fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternE
424500

425501
#[cfg(test)]
426502
mod tests {
427-
use std::collections::HashSet;
503+
use std::{collections::HashSet, sync::Arc};
428504

429505
use glob::Pattern;
430506

507+
use crate::fs::ScopeInner;
508+
431509
use super::{push_pattern, Scope};
432510

433511
fn new_scope() -> Scope {
434512
Scope {
435-
allowed_patterns: Default::default(),
436-
forbidden_patterns: Default::default(),
437-
event_listeners: Default::default(),
438-
next_event_id: Default::default(),
439-
match_options: glob::MatchOptions {
440-
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
441-
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
442-
require_literal_separator: true,
443-
// dotfiles are not supposed to be exposed by default on unix
444-
#[cfg(unix)]
445-
require_literal_leading_dot: true,
446-
#[cfg(windows)]
447-
require_literal_leading_dot: false,
448-
..Default::default()
449-
},
513+
inner: Arc::new(ScopeInner {
514+
allowed_patterns: Default::default(),
515+
forbidden_patterns: Default::default(),
516+
event_listeners: Default::default(),
517+
pending: Default::default(),
518+
emitting: Default::default(),
519+
next_event_id: Default::default(),
520+
match_options: glob::MatchOptions {
521+
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
522+
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
523+
require_literal_separator: true,
524+
// dotfiles are not supposed to be exposed by default on unix
525+
#[cfg(unix)]
526+
require_literal_leading_dot: true,
527+
#[cfg(windows)]
528+
require_literal_leading_dot: false,
529+
..Default::default()
530+
},
531+
}),
450532
}
451533
}
452534

@@ -645,4 +727,15 @@ mod tests {
645727
assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**");
646728
}
647729
}
730+
731+
#[test]
732+
fn event_no_deadlocks() {
733+
let scope = new_scope();
734+
let scope_clone = scope.clone();
735+
scope.once(move |event| {
736+
assert!(matches!(event, super::Event::PathAllowed(_)));
737+
scope_clone.allow_file("/another-test-path").unwrap();
738+
});
739+
scope.allow_file("/test-path").unwrap();
740+
}
648741
}

0 commit comments

Comments
 (0)