Skip to main content

tauri_runtime_cef/
environment.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! The diagnostic environment variables Chromium and CEF read behind the application's
6//! back, and the policy that decides whether they are honoured.
7//!
8//! Chromium is a browser, and a browser is expected to let whoever runs it capture its
9//! own traffic and redirect its own crash reports. An application that embeds Chromium
10//! inherits those hooks without asking for them, and in a shipped application they are
11//! not developer conveniences any more: anyone who can set a variable in the
12//! application's environment can decrypt every TLS session it makes, or point its
13//! minidumps — which carry process memory — at a server of their choosing.
14//!
15//! [`DebugEnvironment`] is the switch. Neither variable group is reachable through a CEF
16//! setting, so each is answered where Chromium reads it.
17//!
18//! # TLS key logging is answered on the command line
19//!
20//! `content/browser/network_service_instance_impl.cc` consults `SSLKEYLOGFILE` only when
21//! `--ssl-key-log-file` is absent, and an empty switch value logs a warning and creates no
22//! key logger. Appending the empty switch is therefore a complete answer that needs no
23//! change to the process environment, and the runtime appends it only when the variable is
24//! actually set — so the warning appears exactly when somebody was trying to log keys.
25//!
26//! # The crash reporter overrides are answered in the environment
27//!
28//! CEF reads its three crash variables from `BasicStartupComplete`, before any hook the
29//! embedder can install, so there is nothing to append and the variables have to go. That
30//! write is [`std::env::remove_var`], which is why it happens once, during runtime
31//! initialization, and only for a variable that is actually set.
32
33/// Whether Chromium and CEF may read their diagnostic environment variables.
34#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum DebugEnvironment {
37  /// Honour them in development builds (`tauri::is_dev()`), refuse them in release
38  /// builds.
39  #[default]
40  Auto,
41  /// Always honour them.
42  ///
43  /// Appropriate for a build whose users are expected to debug it — an internal tool, a
44  /// QA build — and wrong for one handed to the public.
45  Allow,
46  /// Always refuse them, in every build profile.
47  Deny,
48}
49
50/// The environment variable whose answer is the `--ssl-key-log-file` switch.
51///
52/// The network service writes the pre-master secret of every TLS session to the file it
53/// names, which is exactly what a packet capture needs to decrypt the application's
54/// traffic in full.
55const SSL_KEY_LOG_FILE: &str = "SSLKEYLOGFILE";
56
57/// CEF's crash reporter overrides, and what each one does.
58///
59/// * `CEF_CRASH_REPORTER_SERVER_URL` — replaces the crash report upload URL from
60///   `crash_reporter.cfg`. A minidump carries stack and heap memory, so a redirected URL
61///   is a memory exfiltration channel.
62/// * `CEF_CRASH_REPORTER_RATE_LIMIT_ENABLED` — lifts the upload rate limit that keeps a
63///   crash loop from becoming a flood.
64/// * `BREAKPAD_DUMP_LOCATION` — redirects where minidumps are written on Windows.
65///
66/// All three are only consulted when the application ships a `crash_reporter.cfg`, so
67/// removing them costs nothing when it does not.
68const CRASH_REPORTER_VARIABLES: &[(&str, &str)] = &[
69  (
70    "CEF_CRASH_REPORTER_SERVER_URL",
71    "redirecting crash report uploads, which carry process memory",
72  ),
73  (
74    "CEF_CRASH_REPORTER_RATE_LIMIT_ENABLED",
75    "overriding the crash report upload rate limit",
76  ),
77  (
78    "BREAKPAD_DUMP_LOCATION",
79    "redirecting where minidumps are written",
80  ),
81];
82
83/// Whether `policy` refuses the variables in a build where `is_dev` says what it says.
84///
85/// Kept separate from acting on the answer so the decision can be unit tested without
86/// touching the process environment.
87fn refuses_debug_variables(policy: DebugEnvironment, is_dev: bool) -> bool {
88  match policy {
89    DebugEnvironment::Auto => !is_dev,
90    DebugEnvironment::Allow => false,
91    DebugEnvironment::Deny => true,
92  }
93}
94
95/// Whether a variable is set to something Chromium would act on.
96fn is_set(name: &str) -> bool {
97  std::env::var_os(name).is_some_and(|value| !value.is_empty())
98}
99
100/// Whether the runtime should append an empty `--ssl-key-log-file`, which is how
101/// `SSLKEYLOGFILE` is refused.
102///
103/// Answers `false` when the variable is not set, so the switch — and the warning Chromium
104/// logs for it — only appears when something was actually asking for a key log.
105pub(crate) fn neutralizes_tls_key_log(policy: DebugEnvironment, is_dev: bool) -> bool {
106  if !refuses_debug_variables(policy, is_dev) || !is_set(SSL_KEY_LOG_FILE) {
107    return false;
108  }
109
110  log::warn!(
111    "ignoring the {SSL_KEY_LOG_FILE} environment variable: it asks Chromium to log the TLS \
112     session keys that decrypt this application's network traffic. Set \
113     DebugEnvironment::Allow to honour it."
114  );
115  true
116}
117
118/// Removes CEF's crash reporter overrides from the process environment when `policy`
119/// refuses them.
120///
121/// Must run before the first CEF call: CEF reads these from
122/// `ChromeMainDelegateCef::BasicStartupComplete`, which `cef::initialize` reaches, and
123/// child processes inherit the environment of the browser process that spawned them, so a
124/// variable removed here is gone from the whole process tree.
125///
126/// # Safety
127///
128/// Calls [`std::env::remove_var`], which is unsound while another thread reads or writes
129/// the environment concurrently. The caller must be the runtime's own initialization,
130/// which runs on the main thread before CEF exists and before this runtime starts a
131/// thread of its own.
132pub(crate) fn remove_crash_reporter_overrides(policy: DebugEnvironment, is_dev: bool) {
133  if !refuses_debug_variables(policy, is_dev) {
134    return;
135  }
136
137  for (name, effect) in CRASH_REPORTER_VARIABLES {
138    if !is_set(name) {
139      continue;
140    }
141
142    log::warn!(
143      "ignoring the {name} environment variable: it asks CEF for {effect}. Set \
144       DebugEnvironment::Allow to honour it."
145    );
146    // SAFETY: documented on this function; the runtime applies the policy from its own
147    // initialization, before CEF exists and before it starts a thread of its own.
148    unsafe { std::env::remove_var(name) };
149  }
150}
151
152#[cfg(test)]
153mod tests {
154  use super::*;
155
156  #[test]
157  fn auto_follows_the_build_profile() {
158    assert!(
159      !refuses_debug_variables(DebugEnvironment::Auto, true),
160      "a development build keeps its debugging hooks"
161    );
162    assert!(
163      refuses_debug_variables(DebugEnvironment::Auto, false),
164      "a shipped build must not hand its TLS keys to whoever sets a variable"
165    );
166  }
167
168  #[test]
169  fn explicit_policies_ignore_the_build_profile() {
170    for is_dev in [false, true] {
171      assert!(!refuses_debug_variables(DebugEnvironment::Allow, is_dev));
172      assert!(refuses_debug_variables(DebugEnvironment::Deny, is_dev));
173    }
174  }
175
176  #[test]
177  fn a_permissive_policy_never_touches_the_command_line() {
178    // Asserted without depending on the ambient environment: `Allow` short-circuits
179    // before the variable is even read.
180    assert!(!neutralizes_tls_key_log(DebugEnvironment::Allow, false));
181    assert!(!neutralizes_tls_key_log(DebugEnvironment::Auto, true));
182  }
183
184  #[test]
185  fn every_crash_variable_is_described() {
186    // The name and the effect both go into a warning the user reads.
187    for (name, effect) in CRASH_REPORTER_VARIABLES {
188      assert!(!name.is_empty());
189      assert!(!effect.is_empty());
190    }
191  }
192
193  #[test]
194  fn the_tls_variable_is_not_also_removed_from_the_environment() {
195    // It is answered on the command line instead, which needs no environment write.
196    assert!(
197      !CRASH_REPORTER_VARIABLES
198        .iter()
199        .any(|(name, _)| *name == SSL_KEY_LOG_FILE)
200    );
201  }
202}