Skip to content

Commit 7c3db7a

Browse files
chipperslucasfernog
andcommitted
cache current binary path much sooner (#45)
* use ctor to cache starting executable * clean up symlink checking logic * changefile * use wrapper for the static, put it in tauri_utils * cargo +nightly fmt * add license header to `StartingBinary` * fix clippy warning * fix: test * simplify macOS dangerous flag detection * update restart test to allow expected failure on macOS * finish documentation Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
1 parent b82e2b5 commit 7c3db7a

14 files changed

Lines changed: 580 additions & 578 deletions

File tree

.changes/current-binary-caching.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
tauri: patch
3+
tauri-utils: patch
4+
---
5+
6+
The path returned from `tauri::api::process::current_binary` is now cached when loading the binary.

core/tauri-utils/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ aes-gcm = { version = "0.9", optional = true }
3030
ring = { version = "0.16", optional = true, features = ["std"] }
3131
once_cell = { version = "1.8", optional = true }
3232
serialize-to-javascript = { git = "https://github.com/chippers/serialize-to-javascript" }
33+
ctor = "0.1"
3334

3435
[target."cfg(target_os = \"linux\")".dependencies]
3536
heck = "0.4"
@@ -39,3 +40,4 @@ build = [ "proc-macro2", "quote" ]
3940
compression = [ "zstd" ]
4041
schema = ["schemars"]
4142
isolation = [ "aes-gcm", "ring", "once_cell" ]
43+
process-relaunch-dangerous-allow-symlink-macos = []

core/tauri-utils/src/config.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,12 @@ pub struct ProcessAllowlistConfig {
12601260
/// Enables the relaunch API.
12611261
#[serde(default)]
12621262
pub relaunch: bool,
1263+
/// Dangerous option that allows macOS to relaunch even if the binary contains a symlink.
1264+
///
1265+
/// This is due to macOS having less symlink protection. Highly recommended to not set this flag
1266+
/// unless you have a very specific reason too, and understand the implications of it.
1267+
#[serde(default)]
1268+
pub relaunch_dangerous_allow_symlink_macos: bool,
12631269
/// Enables the exit API.
12641270
#[serde(default)]
12651271
pub exit: bool,
@@ -1270,6 +1276,7 @@ impl Allowlist for ProcessAllowlistConfig {
12701276
let allowlist = Self {
12711277
all: false,
12721278
relaunch: true,
1279+
relaunch_dangerous_allow_symlink_macos: false,
12731280
exit: true,
12741281
};
12751282
let mut features = allowlist.to_features();
@@ -1283,6 +1290,12 @@ impl Allowlist for ProcessAllowlistConfig {
12831290
} else {
12841291
let mut features = Vec::new();
12851292
check_feature!(self, features, relaunch, "process-relaunch");
1293+
check_feature!(
1294+
self,
1295+
features,
1296+
relaunch_dangerous_allow_symlink_macos,
1297+
"process-relaunch-dangerous-allow-symlink-macos"
1298+
);
12861299
check_feature!(self, features, exit, "process-exit");
12871300
features
12881301
}

core/tauri-utils/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ impl Default for Env {
6868
// an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}`
6969
// see https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501
7070
// note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage.
71-
if !std::env::current_exe()
71+
let is_temp = std::env::current_exe()
7272
.map(|p| {
7373
p.display()
7474
.to_string()
7575
.starts_with(&format!("{}/.mount_", std::env::temp_dir().display()))
7676
})
77-
.unwrap_or(true)
78-
{
77+
.unwrap_or(true);
78+
79+
if !is_temp {
7980
panic!("`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue.");
8081
}
8182
}

core/tauri-utils/src/platform.rs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,82 @@ use std::path::{PathBuf, MAIN_SEPARATOR};
88

99
use crate::{Env, PackageInfo};
1010

11-
/// Gets the path to the current executable, resolving symbolic links for security reasons.
11+
mod starting_binary;
12+
13+
/// Retrieves the currently running binary's path, taking into account security considerations.
14+
///
15+
/// The path is cached as soon as possible (before even `main` runs) and that value is returned
16+
/// repeatedly instead of fetching the path every time. It is possible for the path to not be found,
17+
/// or explicitly disabled (see following macOS specific behavior).
18+
///
19+
/// # Platform-specific behavior
20+
///
21+
/// On `macOS`, this function will return an error if the original path contained any symlinks
22+
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
23+
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
24+
///
25+
/// # Security
26+
///
27+
/// If the above platform-specific behavior does **not** take place, this function uses the
28+
/// following resolution.
29+
///
30+
/// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links.
31+
/// This avoids the usual issue of needing the file to exist at the passed path because a valid
32+
/// current executable result for our purpose should always exist. Notably,
33+
/// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using
34+
/// hard links. Let's cover some specific topics that relate to different ways an attacker might
35+
/// try to trick this function into returning the wrong binary path.
36+
///
37+
/// ## Symlinks ("Soft Links")
38+
///
39+
/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path,
40+
/// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include
41+
/// a symlink are rejected by default due to lesser symlink protections. This can be disabled,
42+
/// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature.
43+
///
44+
/// ## Hard Links
45+
///
46+
/// A [Hard Link] is a named entry that points to a file in the file system.
47+
/// On most systems, this is what you would think of as a "file". The term is
48+
/// used on filesystems that allow multiple entries to point to the same file.
49+
/// The linked [Hard Link] Wikipedia page provides a decent overview.
50+
///
51+
/// In short, unless the attacker was able to create the link with elevated
52+
/// permissions, it should generally not be possible for them to hard link
53+
/// to a file they do not have permissions to - with exception to possible
54+
/// operating system exploits.
55+
///
56+
/// There are also some platform-specific information about this below.
57+
///
58+
/// ### Windows
59+
///
60+
/// Windows requires a permission to be set for the user to create a symlink
61+
/// or a hard link, regardless of ownership status of the target. Elevated
62+
/// permissions users have the ability to create them.
63+
///
64+
/// ### macOS
65+
///
66+
/// macOS allows for the creation of symlinks and hard links to any file.
67+
/// Accessing through those links will fail if the user who owns the links
68+
/// does not have the proper permissions on the original file.
69+
///
70+
/// ### Linux
71+
///
72+
/// Linux allows for the creation of symlinks to any file. Accessing the
73+
/// symlink will fail if the user who owns the symlink does not have the
74+
/// proper permissions on the original file.
1275
///
13-
/// See https://doc.rust-lang.org/std/env/fn.current_exe.html#security for
14-
/// an example of what to be careful of when using `current_exe` output.
76+
/// Linux additionally provides a kernel hardening feature since version
77+
/// 3.6 (30 September 2012). Most distributions since then have enabled
78+
/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
79+
/// means that a vast majority of desktop Linux users should have it enabled.
80+
/// **The feature prevents the creation of hardlinks that the user does not own
81+
/// or have read/write access to.** [See the patch that enabled this].
1582
///
16-
/// We canonicalize the path we received from `current_exe` to resolve any
17-
/// soft links. it avoids the usual issue of needing the file to exist at
18-
/// the passed path because a valid `current_exe` result should always exist.
83+
/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
84+
/// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7
1985
pub fn current_exe() -> std::io::Result<PathBuf> {
20-
std::env::current_exe().and_then(|path| path.canonicalize())
86+
self::starting_binary::STARTING_BINARY.cloned()
2187
}
2288

2389
/// Try to determine the current target triple.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
use ctor::ctor;
6+
use std::{
7+
io::{Error, ErrorKind, Result},
8+
path::{Path, PathBuf},
9+
};
10+
11+
/// A cached version of the current binary using [`ctor`] to cache it before even `main` runs.
12+
#[ctor]
13+
#[used]
14+
pub(super) static STARTING_BINARY: StartingBinary = StartingBinary::new();
15+
16+
/// Represents a binary path that was cached when the program was loaded.
17+
pub(super) struct StartingBinary(std::io::Result<PathBuf>);
18+
19+
impl StartingBinary {
20+
/// Find the starting executable as safely as possible.
21+
fn new() -> Self {
22+
// see notes on current_exe() for security implications
23+
let dangerous_path = match std::env::current_exe() {
24+
Ok(dangerous_path) => dangerous_path,
25+
error @ Err(_) => return Self(error),
26+
};
27+
28+
// note: this only checks symlinks on problematic platforms, see implementation below
29+
if let Some(symlink) = Self::has_symlink(&dangerous_path) {
30+
return Self(Err(Error::new(
31+
ErrorKind::InvalidData,
32+
format!("StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}", symlink.display()),
33+
)));
34+
}
35+
36+
// we canonicalize the path to resolve any symlinks to the real exe path
37+
Self(dangerous_path.canonicalize())
38+
}
39+
40+
/// A clone of the [`PathBuf`] found to be the starting path.
41+
///
42+
/// Because [`Error`] is not clone-able, it is recreated instead.
43+
pub(super) fn cloned(&self) -> Result<PathBuf> {
44+
self
45+
.0
46+
.as_ref()
47+
.map(Clone::clone)
48+
.map_err(|e| Error::new(e.kind(), e.to_string()))
49+
}
50+
51+
/// We only care about checking this on macOS currently, as it has the least symlink protections.
52+
#[cfg(any(
53+
not(target_os = "macos"),
54+
feature = "process-relaunch-dangerous-allow-symlink-macos"
55+
))]
56+
fn has_symlink(_: &Path) -> Option<&Path> {
57+
None
58+
}
59+
60+
/// We only care about checking this on macOS currently, as it has the least symlink protections.
61+
#[cfg(all(
62+
target_os = "macos",
63+
not(feature = "process-relaunch-dangerous-allow-symlink-macos")
64+
))]
65+
fn has_symlink(path: &Path) -> Option<&Path> {
66+
path.ancestors().find(|ancestor| {
67+
matches!(
68+
ancestor
69+
.symlink_metadata()
70+
.as_ref()
71+
.map(std::fs::Metadata::is_symlink),
72+
Ok(true)
73+
)
74+
})
75+
}
76+
}

core/tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ tokio-test = "0.4.2"
101101
tokio = { version = "1.15", features = [ "full" ] }
102102

103103
[target."cfg(windows)".dev-dependencies.windows]
104-
version = "0.29.0"
104+
version = "0.30.0"
105105
features = [
106106
"Win32_Foundation",
107107
]
@@ -172,6 +172,7 @@ path-all = []
172172
process-all = ["process-relaunch", "process-exit"]
173173
process-exit = []
174174
process-relaunch = []
175+
process-relaunch-dangerous-allow-symlink-macos = ["tauri-utils/process-relaunch-dangerous-allow-symlink-macos"]
175176
protocol-all = ["protocol-asset"]
176177
protocol-asset = []
177178
shell-all = ["shell-execute", "shell-sidecar", "shell-open"]

core/tauri/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ fn main() {
9090
// process
9191
process_all: { any(api_all, feature = "process-all") },
9292
process_relaunch: { any(protocol_all, feature = "process-relaunch") },
93+
process_relaunch_dangerous_allow_symlink_macos: { feature = "process-relaunch-dangerous-allow-symlink-macos" },
9394
process_exit: { any(protocol_all, feature = "process-exit") },
9495

9596
// clipboard

core/tauri/src/api/process.rs

Lines changed: 21 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,83 +17,46 @@ pub use command::*;
1717

1818
/// Finds the current running binary's path.
1919
///
20-
/// # Platform-specific behavior
21-
///
22-
/// On the `Linux` platform, this function will also **attempt** to detect if
23-
/// it's currently running from a valid [AppImage] and use that path instead.
24-
///
25-
/// # Security
26-
///
27-
/// If the above Platform-specific behavior does not take place, this function
28-
/// uses [`std::env::current_exe`]. Notably, it also has a security section
29-
/// that goes over a theoretical attack using hard links. Let's cover some
30-
/// specific topics that relate to different ways an attacker might try to
31-
/// trick this function into returning the wrong binary path.
32-
///
33-
/// ## Symlinks ("Soft Links")
34-
///
35-
/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the
36-
/// original path, including nested symbolic links (`link2 -> link1 -> bin`).
20+
/// With exception to any following platform-specific behavior, the path is cached as soon as
21+
/// possible, and then used repeatedly instead of querying for a new path every time this function
22+
/// is called.
3723
///
38-
/// ## Hard Links
39-
///
40-
/// A [Hard Link] is a named entry that points to a file in the file system.
41-
/// On most systems, this is what you would think of as a "file". The term is
42-
/// used on filesystems that allow multiple entries to point to the same file.
43-
/// The linked [Hard Link] Wikipedia page provides a decent overview.
44-
///
45-
/// In short, unless the attacker was able to create the link with elevated
46-
/// permissions, it should generally not be possible for them to hard link
47-
/// to a file they do not have permissions to - with exception to possible
48-
/// operating system exploits.
49-
///
50-
/// There are also some platform-specific information about this below.
51-
///
52-
/// ### Windows
24+
/// # Platform-specific behavior
5325
///
54-
/// Windows requires a permission to be set for the user to create a symlink
55-
/// or a hard link, regardless of ownership status of the target. Elevated
56-
/// permissions users have the ability to create them.
26+
/// ## Linux
5727
///
58-
/// ### macOS
28+
/// On Linux, this function will **attempt** to detect if it's currently running from a
29+
/// valid [AppImage] and use that path instead.
5930
///
60-
/// macOS allows for the creation of symlinks and hard links to any file.
61-
/// Accessing through those links will fail if the user who owns the links
62-
/// does not have the proper permissions on the original file.
31+
/// ## macOS
6332
///
64-
/// ### Linux
33+
/// On `macOS`, this function will return an error if the original path contained any symlinks
34+
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
35+
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
6536
///
66-
/// Linux allows for the creation of symlinks to any file. Accessing the
67-
/// symlink will fail if the user who owns the symlink does not have the
68-
/// proper permissions on the original file.
37+
/// # Security
6938
///
70-
/// Linux additionally provides a kernel hardening feature since version
71-
/// 3.6 (30 September 2012). Most distributions since then have enabled
72-
/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
73-
/// means that a vast majority of desktop Linux users should have it enabled.
74-
/// **The feature prevents the creation of hardlinks that the user does not own
75-
/// or have read/write access to.** [See the patch that enabled this.](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7)
39+
/// See [`tauri_utils::platform::current_exe`] for possible security implications.
7640
///
7741
/// [AppImage]: https://appimage.org/
78-
/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
79-
#[allow(unused_variables)]
80-
pub fn current_binary(env: &Env) -> Option<PathBuf> {
42+
pub fn current_binary(_env: &Env) -> std::io::Result<PathBuf> {
8143
// if we are running from an AppImage, we ONLY want the set AppImage path
8244
#[cfg(target_os = "linux")]
83-
if let Some(app_image_path) = &env.appimage {
84-
return Some(PathBuf::from(app_image_path));
45+
if let Some(app_image_path) = &_env.appimage {
46+
return Ok(PathBuf::from(app_image_path));
8547
}
8648

87-
tauri_utils::platform::current_exe().ok()
49+
tauri_utils::platform::current_exe()
8850
}
8951

90-
/// Restarts the process.
52+
/// Restarts the currently running binary.
9153
///
92-
/// See [`current_binary`] for the possible security implications.
54+
/// See [`current_binary`] for platform specific behavior, and
55+
/// [`tauri_utils::platform::current_exe`] for possible security implications.
9356
pub fn restart(env: &Env) {
9457
use std::process::{exit, Command};
9558

96-
if let Some(path) = current_binary(env) {
59+
if let Ok(path) = current_binary(env) {
9760
Command::new(path)
9861
.spawn()
9962
.expect("application failed to start");

core/tauri/src/scope/shell.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,15 @@ impl Scope {
146146
}
147147
})
148148
.collect(),
149-
(Some(list), arg) if arg.is_empty() && list.iter().all(ShellScopeAllowedArg::is_fixed) => list
150-
.iter()
151-
.map(|arg| match arg {
152-
ShellScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
153-
_ => unreachable!(),
154-
})
155-
.collect(),
149+
(Some(list), arg) if arg.is_empty() && list.iter().all(ShellScopeAllowedArg::is_fixed) => {
150+
list
151+
.iter()
152+
.map(|arg| match arg {
153+
ShellScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
154+
_ => unreachable!(),
155+
})
156+
.collect()
157+
}
156158
(Some(list), _) if list.is_empty() => Err(ScopeError::InvalidInput(command_name.into())),
157159
(Some(_), _) => Err(ScopeError::InvalidInput(command_name.into())),
158160
}?;

0 commit comments

Comments
 (0)