Skip to content
Closed
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .changes/fix-state-manager-unsoundness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
tauri: 'minor:bug'
---

Fix `use-after-free` unsoundness for using `State::inner` after `Manager::unmanage`, see tauri-apps/tauri#12721 for details.
2 changes: 1 addition & 1 deletion bench/tests/files_transfer/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fn app_should_close(exit_code: i32) {
#[command]
async fn read_file<R: Runtime>(app: AppHandle<R>) -> Result<Response, String> {
let path = app
.path()
.path_arc()
.resolve(".tauri_3mb.json", BaseDirectory::Home)
.map_err(|e| e.to_string())?;
let contents = read(path).map_err(|e| e.to_string())?;
Expand Down
2 changes: 1 addition & 1 deletion crates/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1981,7 +1981,7 @@ tauri::Builder::default()
if let crate::utils::config::WebviewInstallMode::FixedRuntime { path } =
&app.manager.config().bundle.windows.webview_install_mode
{
if let Ok(resource_dir) = app.path().resource_dir() {
if let Ok(resource_dir) = app.path_arc().resource_dir() {
std::env::set_var(
"WEBVIEW2_BROWSER_EXECUTABLE_FOLDER",
resource_dir.join(path),
Expand Down
4 changes: 2 additions & 2 deletions crates/tauri/src/ipc/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ impl ScopeManager {
key: &str,
) -> crate::Result<ScopeValue<T>> {
match self.global_scope_cache.try_get::<ScopeValue<T>>() {
Some(cached) => Ok(cached.inner().clone()),
Some(cached) => Ok((*cached).clone()),
None => {
let mut allow = Vec::new();
let mut deny = Vec::new();
Expand Down Expand Up @@ -891,7 +891,7 @@ impl ScopeManager {
) -> crate::Result<ScopeValue<T>> {
let cache = self.command_cache.get(key).unwrap();
match cache.try_get::<ScopeValue<T>>() {
Some(cached) => Ok(cached.inner().clone()),
Some(cached) => Ok((*cached).clone()),
None => {
let resolved_scope = self
.command_scope
Expand Down
46 changes: 40 additions & 6 deletions crates/tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ use std::{
borrow::Cow,
collections::HashMap,
fmt::{self, Debug},
sync::MutexGuard,
sync::{Arc, MutexGuard},
};
use utils::assets::{AssetKey, CspHash, EmbeddedAssets};

Expand Down Expand Up @@ -710,12 +710,33 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
self.manager().state().set(state)
}

/// Removes the state managed by the application for T. Returns the state if it was actually removed.
/// Removes the state managed by the application for T. Returns the state if it was actually removed and no other [Arc] clones exist.
///
/// <div class="warning">
///
/// To fix [tauri-apps/tauri#12721], the internal structure of [State] changed in version `2.3`.
/// So This method is equivalent to [Manager::unmanage_arc] and [Arc::into_inner] now,
/// which means that even if the state is actually removed, this method will return None if there are other Arc clones of the state.
/// Please refer to [[Arc::into_inner]] for details.
///
/// [tauri-apps/tauri#12721]: https://github.com/tauri-apps/tauri/issues/12721
///
/// </div>
#[deprecated(since = "2.3.0", note = "use `unmanage_arc` instead")]
fn unmanage<T>(&self) -> Option<T>
where
T: Send + Sync + 'static,
{
self.manager().state().unmanage()
let state = self.unmanage_arc::<T>()?;
Arc::into_inner(state)
}

/// Removes the state managed by the application for T. Returns the state if it was actually removed.
fn unmanage_arc<T>(&self) -> Option<Arc<T>>
where
T: Send + Sync + 'static,
{
self.manager().state().unmanage_arc()
}

/// Retrieves the managed state for the type `T`.
Expand Down Expand Up @@ -750,20 +771,33 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {

/// Gets the managed [`Env`].
fn env(&self) -> Env {
self.state::<Env>().inner().clone()
(*self.state::<Env>()).clone()
}

/// Gets the scope for the asset protocol.
#[cfg(feature = "protocol-asset")]
fn asset_protocol_scope(&self) -> scope::fs::Scope {
self.state::<Scopes>().inner().asset_protocol.clone()
self.state::<Scopes>().asset_protocol.clone()
}

/// The path resolver.
/// The path resolver.(deprecated)
///
/// <div class="warning">
///
/// This method will cause a memory leak, See [State::inner] for details.
///
/// </div>
#[deprecated(since = "2.3.0", note = "use `Manager::path_arc` instead")]
fn path(&self) -> &crate::path::PathResolver<R> {
#[expect(deprecated)]
self.state::<crate::path::PathResolver<R>>().inner()
}

/// The path resolver.
fn path_arc(&self) -> Arc<crate::path::PathResolver<R>> {
self.state::<crate::path::PathResolver<R>>().into_inner()
}

/// Adds a capability to the app.
///
/// Note that by default every capability file in the `src-tauri/capabilities` folder
Expand Down
2 changes: 1 addition & 1 deletion crates/tauri/src/manager/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ impl<R: Runtime> WebviewManager<R> {
// but we do respect user-specification
#[cfg(any(target_os = "linux", target_os = "windows"))]
if pending.webview_attributes.data_directory.is_none() {
let local_app_data = manager.path().resolve(
let local_app_data = manager.path_arc().resolve(
&app_manager.config.identifier,
crate::path::BaseDirectory::LocalData,
);
Expand Down
4 changes: 2 additions & 2 deletions crates/tauri/src/scope/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,15 @@ impl Scope {
) -> crate::Result<Self> {
let mut allowed_patterns = HashSet::new();
for path in scope.allowed_paths() {
if let Ok(path) = manager.path().parse(path) {
if let Ok(path) = manager.path_arc().parse(path) {
push_pattern(&mut allowed_patterns, path, Pattern::new)?;
}
}

let mut forbidden_patterns = HashSet::new();
if let Some(forbidden_paths) = scope.forbidden_paths() {
for path in forbidden_paths {
if let Ok(path) = manager.path().parse(path) {
if let Ok(path) = manager.path_arc().parse(path) {
push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
}
}
Expand Down
118 changes: 74 additions & 44 deletions crates/tauri/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,74 @@

use std::{
any::{Any, TypeId},
cell::UnsafeCell,
collections::HashMap,
hash::BuildHasherDefault,
sync::Mutex,
marker::PhantomData,
ops::Deref,
sync::{Arc, Mutex},
};

use crate::{
ipc::{CommandArg, CommandItem, InvokeError},
Runtime,
};

type Wrapper<T> = Arc<T>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This just makes it confusing, let's remove and use Arc directly


/// A guard for a state value.
///
/// See [`Manager::manage`](`crate::Manager::manage`) for usage examples.
pub struct State<'r, T: Send + Sync + 'static>(&'r T);
pub struct State<'r, T: Send + Sync + 'static>(
Wrapper<T>,
// 👇 TODO: just for 2.x semantic compatibility, we can't remove `'r` for now
PhantomData<&'r ()>,
);

impl<'r, T: Send + Sync + 'static> State<'r, T> {
/// Retrieve a borrow to the underlying value with a lifetime of `'r`.
/// Using this method is typically unnecessary as `State` implements
/// [`std::ops::Deref`] with a [`std::ops::Deref::Target`] of `T`.
///
/// <div class="warning">
///
/// To fix [tauri-apps/tauri#12721], the internal structure of [State] changed in version `2.3`.
/// So This method is equivalent to [Self::into_inner] and [Arc::into_raw] now, which means it may cause memory leaks.
/// Please refer to [[Arc::into_raw]] for related limitations.
///
/// [tauri-apps/tauri#12721]: https://github.com/tauri-apps/tauri/issues/12721
///
/// </div>
#[inline(always)]
#[deprecated(
since = "2.3.0",
note = "will cause memory leak, use `into_inner` or `Deref::deref` instead"
)]
pub fn inner(&self) -> &'r T {
let ptr = Arc::into_raw(self.0.clone());
// SAFETY: this ptr is valid, because we just created it;
// and the lifetime is 'static (it's leaked), so it's safe to return it.
unsafe { &*ptr }
}
Comment on lines +45 to +54

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

a simple fix for this would be

 pub fn inner(&'r self) -> &'r T {
   self.0.as_ref()
 }

In normal cases, this should be a breaking change but since this fixes a memory leak, it should be fine

@WSH032 WSH032 Feb 20, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I personally believe that most people do not need the unmanage feature (it seems that tauri itself does not use it also); if users really need unmanage, they can implement it themselves using Mutex and Option::take. This way, users who do not need mutability (unmanage) do not have to pay the performance cost (use Arc instead of Box, and Arc::clone for every get).

So, does tauri really want to introduce a BREAKING CHANGE in 2.x for this? Moreover, this BREAKING CHANGE would make pub fn inner(&'r self) -> &'r T quite useless (almost equivalent to removing it).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I assume you mean app.state().inner() use-case won't work? in that case I guess your solution is correct, and deref should be used instead.

@WSH032 WSH032 Feb 20, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here is my use case for fn inner: https://github.com/WSH032/pytauri/blob/47fe27d6f67c6d490ce753ac7976eb4b8a43cbdd/crates/tauri-plugin-pytauri/src/lib.rs#L103-L109.

With fn inner<'a>(&'a self) -> &'r T, I can return a reference of another type without needing to continue holding State.

If we really want to proceed as mentioned above, we might need to provide a method similar to MutexGuard::map to help convert State<'_, T> to State<'_, U>.

@amrbashir amrbashir Feb 20, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

at this point, I would go with #12723 for now, but instead of completely removing, we just document its pitfall and maybe print a warning in debug builds.

then in v3, we rework the API and introduce .map and remove inner in favor of deref


/// Retrieve the inner value of the [State].
/// For `2.0` semantic compatibility, we cannot directly return [Arc<T>] in [crate::Manager::state].
pub fn into_inner(self) -> Arc<T> {
self.0
}
}

impl<T: Send + Sync + 'static> std::ops::Deref for State<'_, T> {
impl<T: Send + Sync + 'static> Deref for State<'_, T> {
type Target = T;

#[inline(always)]
fn deref(&self) -> &T {
self.0
Deref::deref(&self.0)
}
}

impl<T: Send + Sync + 'static> Clone for State<'_, T> {
fn clone(&self) -> Self {
State(self.0)
State(Wrapper::clone(&self.0), PhantomData)
}
}

Expand Down Expand Up @@ -97,12 +127,13 @@ impl std::hash::Hasher for IdentHash {
}
}

type TypeIdMap = HashMap<TypeId, Box<dyn Any>, BuildHasherDefault<IdentHash>>;
/// The `key` must equal to `value.type_id()`, see the safety doc in methods of [StateManager] for details.
type TypeIdMap = HashMap<TypeId, Box<dyn Any + Sync + Send>, BuildHasherDefault<IdentHash>>;

/// The Tauri state manager.
#[derive(Debug)]
pub struct StateManager {
map: Mutex<UnsafeCell<TypeIdMap>>,
map: Mutex<TypeIdMap>,
}

// SAFETY: data is accessed behind a lock
Expand All @@ -116,37 +147,32 @@ impl StateManager {
}
}

fn with_map_ref<'a, F: FnOnce(&'a TypeIdMap) -> R, R>(&'a self, f: F) -> R {
let map = self.map.lock().unwrap();
let map = map.get();
// SAFETY: safe to access since we are holding a lock
f(unsafe { &*map })
}

fn with_map_mut<F: FnOnce(&mut TypeIdMap) -> R, R>(&self, f: F) -> R {
let mut map = self.map.lock().unwrap();
let map = map.get_mut();
f(map)
}

pub(crate) fn set<T: Send + Sync + 'static>(&self, state: T) -> bool {
self.with_map_mut(|map| {
let type_id = TypeId::of::<T>();
let already_set = map.contains_key(&type_id);
if !already_set {
map.insert(type_id, Box::new(state) as Box<dyn Any>);
}
!already_set
})
let mut map = self.map.lock().unwrap();
let type_id = TypeId::of::<Wrapper<T>>();
let already_set = map.contains_key(&type_id);
if !already_set {
map.insert(
type_id,
// SAFETY: keep the type of the key is the same as the type of the value,
// see following methods for details.
Box::new(Wrapper::new(state)) as Box<dyn Any + Sync + Send>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we could remove Box and use Arc directly, should avoid a level of indirection

);
}
!already_set
}

pub(crate) fn unmanage<T: Send + Sync + 'static>(&self) -> Option<T> {
self.with_map_mut(|map| {
let type_id = TypeId::of::<T>();
map
.remove(&type_id)
.and_then(|ptr| ptr.downcast().ok().map(|b| *b))
})
pub(crate) fn unmanage_arc<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
let mut map = self.map.lock().unwrap();
let type_id = TypeId::of::<Wrapper<T>>();
let ptr = map.remove(&type_id)?;
let value = unsafe {
ptr
.downcast::<Wrapper<T>>()
// SAFETY: the type of the key is the same as the type of the value
.unwrap_unchecked()
};
Some(*value)
}

/// Gets the state associated with the specified type.
Expand All @@ -158,12 +184,16 @@ impl StateManager {

/// Gets the state associated with the specified type.
pub fn try_get<T: Send + Sync + 'static>(&self) -> Option<State<'_, T>> {
self.with_map_ref(|map| {
map
.get(&TypeId::of::<T>())
.and_then(|ptr| ptr.downcast_ref::<T>())
.map(State)
})
let map = self.map.lock().unwrap();
let type_id = TypeId::of::<Wrapper<T>>();
let ptr = map.get(&type_id)?;
let value = unsafe {
ptr
.downcast_ref::<Wrapper<T>>()
// SAFETY: the type of the key is the same as the type of the value
.unwrap_unchecked()
};
Some(State(Wrapper::clone(value), PhantomData))
}
}

Expand Down Expand Up @@ -197,8 +227,8 @@ mod tests {
let state = StateManager::new();
assert!(state.set(1u32));
assert_eq!(*state.get::<u32>(), 1);
assert!(state.unmanage::<u32>().is_some());
assert!(state.unmanage::<u32>().is_none());
assert!(state.unmanage_arc::<u32>().is_some());
assert!(state.unmanage_arc::<u32>().is_none());
assert_eq!(state.try_get::<u32>(), None);
assert!(state.set(2u32));
assert_eq!(*state.get::<u32>(), 2);
Expand Down
8 changes: 4 additions & 4 deletions examples/api/src-tauri/tauri-plugin-sample/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT

use serde::Deserialize;
use std::path::PathBuf;
use std::{path::PathBuf, sync::Arc};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
Expand All @@ -28,12 +28,12 @@ pub use error::*;

/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the sample APIs.
pub trait SampleExt<R: Runtime> {
fn sample(&self) -> &Sample<R>;
fn sample(&self) -> Arc<Sample<R>>;
}

impl<R: Runtime, T: Manager<R>> crate::SampleExt<R> for T {
fn sample(&self) -> &Sample<R> {
self.state::<Sample<R>>().inner()
fn sample(&self) -> Arc<Sample<R>> {
self.state::<Sample<R>>().into_inner()
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/resources/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fn main() {
tauri::Builder::default()
.setup(move |app| {
let path = app
.path()
.path_arc()
.resolve("assets/index.js", tauri::path::BaseDirectory::Resource)
.unwrap();

Expand Down