shh: init at 2025.3.13 (#396491)

authored by

Aleksana and committed by
GitHub
bb0452e2 704272fb

+348
+6
maintainers/maintainer-list.nix
··· 13204 13204 githubId = 70764075; 13205 13205 name = "kud"; 13206 13206 }; 13207 + kuflierl = { 13208 + email = "kuflierl@gmail.com"; 13209 + github = "kuflierl"; 13210 + name = "Kennet Flierl"; 13211 + githubId = 41301536; 13212 + }; 13207 13213 kugland = { 13208 13214 email = "kugland@gmail.com"; 13209 13215 github = "kugland";
+201
pkgs/by-name/sh/shh/fix_run_checks.patch
··· 1 + commit 070bf216bacf6ce1b473f2819a017d1be29716d0 2 + Author: kuflierl <41301536+kuflierl@users.noreply.github.com> 3 + Date: Sun Apr 13 19:56:58 2025 +0200 4 + 5 + add support for nix-build-system for tests 6 + 7 + diff --git a/Cargo.toml b/Cargo.toml 8 + index eba0ef8..9153f00 100644 9 + --- a/Cargo.toml 10 + +++ b/Cargo.toml 11 + @@ -58,6 +58,7 @@ default = [] 12 + as-root = [] # for tests only 13 + gen-man-pages = ["dep:clap_mangen"] 14 + nightly = [] # for benchmarks only 15 + +nix-build-env = [] # perform checks in a way compatable with nix build 16 + 17 + [lints.rust] 18 + # https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html 19 + diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs 20 + index e2abbb7..1151592 100644 21 + --- a/src/systemd/resolver.rs 22 + +++ b/src/systemd/resolver.rs 23 + @@ -637,17 +637,14 @@ mod tests { 24 + let OptionValue::List(opt_list) = &candidates[0].value else { 25 + panic!(); 26 + }; 27 + - assert!(opt_list.values.contains(&"/boot".to_owned())); 28 + + // information gathering 29 + + // eprint!("{}\n", &candidates[0].to_string()); 30 + assert!(opt_list.values.contains(&"/dev".to_owned())); 31 + assert!(opt_list.values.contains(&"/etc".to_owned())); 32 + - assert!(opt_list.values.contains(&"/home".to_owned())); 33 + - assert!(opt_list.values.contains(&"/root".to_owned())); 34 + - assert!(opt_list.values.contains(&"/sys".to_owned())); 35 + + assert!(opt_list.values.contains(&"/nix".to_owned())); 36 + + assert!(opt_list.values.contains(&"/bin".to_owned())); 37 + + assert!(opt_list.values.contains(&"/build".to_owned())); 38 + assert!(opt_list.values.contains(&"/tmp".to_owned())); 39 + - assert!(opt_list.values.contains(&"/usr".to_owned())); 40 + - assert!(opt_list.values.contains(&"/var".to_owned())); 41 + - assert!(!opt_list.values.contains(&"/proc".to_owned())); 42 + - assert!(!opt_list.values.contains(&"/run".to_owned())); 43 + 44 + let actions = vec![ProgramAction::Read("/var/data".into())]; 45 + let candidates = resolve(&opts, &actions, &hardening_opts); 46 + diff --git a/tests/options.rs b/tests/options.rs 47 + index 835ee14..cac55e5 100644 48 + --- a/tests/options.rs 49 + +++ b/tests/options.rs 50 + @@ -24,7 +24,7 @@ fn run_true() { 51 + .assert() 52 + .success() 53 + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) 54 + - .stdout(if Uid::effective().is_root() { 55 + + .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") { 56 + BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1)) 57 + } else { 58 + BoxPredicate::new(predicate::str::contains("ProtectHome=").not()) 59 + @@ -50,7 +50,7 @@ fn run_true() { 60 + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) 61 + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) 62 + .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) 63 + - .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 64 + + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 65 + .stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1)); 66 + } 67 + 68 + @@ -97,7 +97,7 @@ fn run_ls_dev() { 69 + .assert() 70 + .success() 71 + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) 72 + - .stdout(if Uid::effective().is_root() { 73 + + .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") { 74 + BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1)) 75 + } else { 76 + BoxPredicate::new(predicate::str::contains("ProtectHome=").not()) 77 + @@ -130,12 +130,12 @@ fn run_ls_dev() { 78 + fn run_ls_proc() { 79 + Command::cargo_bin("shh") 80 + .unwrap() 81 + - .args(["run", "--", "busybox", "ls", "/proc/1/"]) 82 + + .args(["run", "--", "ls", "/proc/1/"]) 83 + .unwrap() 84 + .assert() 85 + .success() 86 + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) 87 + - .stdout(if Uid::effective().is_root() { 88 + + .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") { 89 + BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1)) 90 + } else { 91 + BoxPredicate::new(predicate::str::contains("ProtectHome=").not()) 92 + @@ -166,7 +166,7 @@ fn run_ls_proc() { 93 + .assert() 94 + .success() 95 + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) 96 + - .stdout(if Uid::effective().is_root() { 97 + + .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") { 98 + BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1)) 99 + } else { 100 + BoxPredicate::new(predicate::str::contains("ProtectHome=").not()) 101 + @@ -188,7 +188,7 @@ fn run_ls_proc() { 102 + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) 103 + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) 104 + .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) 105 + - .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 106 + + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 107 + .stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1)); 108 + } 109 + 110 + @@ -201,7 +201,7 @@ fn run_read_kallsyms() { 111 + .assert() 112 + .success() 113 + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) 114 + - .stdout(if Uid::effective().is_root() { 115 + + .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") { 116 + BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1)) 117 + } else { 118 + BoxPredicate::new(predicate::str::contains("ProtectHome=").not()) 119 + @@ -227,11 +227,12 @@ fn run_read_kallsyms() { 120 + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) 121 + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) 122 + .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) 123 + - .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 124 + + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 125 + .stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1)); 126 + } 127 + 128 + #[test] 129 + +#[cfg_attr(feature = "nix-build-env", ignore)] 130 + fn run_ls_modules() { 131 + Command::cargo_bin("shh") 132 + .unwrap() 133 + @@ -240,7 +241,7 @@ fn run_ls_modules() { 134 + .assert() 135 + .success() 136 + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) 137 + - .stdout(if Uid::effective().is_root() { 138 + + .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") { 139 + BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1)) 140 + } else { 141 + BoxPredicate::new(predicate::str::contains("ProtectHome=").not()) 142 + @@ -304,7 +305,7 @@ fn run_dmesg() { 143 + } 144 + 145 + #[test] 146 + -#[cfg_attr(feature = "as-root", ignore)] 147 + +#[cfg_attr(any(feature = "nix-build-env", feature = "as-root"), ignore)] 148 + fn run_systemctl() { 149 + assert!(!Uid::effective().is_root()); 150 + 151 + @@ -344,6 +345,7 @@ fn run_systemctl() { 152 + .stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1)); 153 + } 154 + 155 + +// patched due to nix build isolation 156 + #[test] 157 + fn run_ss() { 158 + Command::cargo_bin("shh") 159 + @@ -353,7 +355,7 @@ fn run_ss() { 160 + .assert() 161 + .success() 162 + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) 163 + - .stdout(if Uid::effective().is_root() { 164 + + .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") { 165 + BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1)) 166 + } else { 167 + BoxPredicate::new(predicate::str::contains("ProtectHome=").not()) 168 + @@ -369,7 +371,7 @@ fn run_ss() { 169 + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) 170 + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) 171 + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) 172 + - .stdout(predicate::str::contains("ProtectProc=").not()) 173 + + //.stdout(predicate::str::contains("ProtectProc=").not()) 174 + .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) 175 + .stdout(predicate::str::contains("RestrictAddressFamilies=AF_NETLINK AF_UNIX\n").count(1).or(predicate::str::contains("RestrictAddressFamilies=AF_NETLINK\n").count(1))) 176 + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) 177 + @@ -379,7 +381,7 @@ fn run_ss() { 178 + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) 179 + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) 180 + .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) 181 + - .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 182 + + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @signal:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) 183 + .stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1)); 184 + } 185 + 186 + @@ -741,6 +743,7 @@ fn run_mknod() { 187 + } 188 + 189 + #[test] 190 + +#[cfg_attr(feature = "nix-build-env", ignore)] // no raw socket cap in nix build 191 + fn run_ping_4() { 192 + Command::cargo_bin("shh") 193 + .unwrap() 194 + @@ -759,6 +762,7 @@ fn run_ping_4() { 195 + } 196 + 197 + #[test] 198 + +#[cfg_attr(feature = "nix-build-env", ignore)] // no raw socket cap in nix build 199 + fn run_ping_6() { 200 + Command::cargo_bin("shh") 201 + .unwrap()
+58
pkgs/by-name/sh/shh/package.nix
··· 1 + { 2 + lib, 3 + rustPlatform, 4 + fetchFromGitHub, 5 + python3, 6 + strace, 7 + systemd, 8 + iproute2, 9 + }: 10 + 11 + rustPlatform.buildRustPackage rec { 12 + pname = "shh"; 13 + version = "2025.4.12"; 14 + 15 + src = fetchFromGitHub { 16 + owner = "desbma"; 17 + repo = "shh"; 18 + tag = "v${version}"; 19 + hash = "sha256-+JWz0ya6gi8pPERnpAcQIe7zZUzWGxha+9/gizMVtEw="; 20 + }; 21 + 22 + cargoHash = "sha256-TdP+1sb1GEFM57z+rc+gqhoWQhPAXzvMt/FCWf3wpr8="; 23 + 24 + patches = [ 25 + ./fix_run_checks.patch 26 + ./pr13-profile-path-fix-strace.patch 27 + ]; 28 + 29 + # buildFeatures = [ /*"gen-man-pages"*/ ]; 30 + 31 + checkFeatures = [ "nix-build-env" ]; 32 + 33 + buildInputs = [ 34 + strace 35 + systemd 36 + ]; 37 + 38 + nativeCheckInputs = [ 39 + strace 40 + systemd 41 + python3 42 + iproute2 43 + ]; 44 + 45 + # RUST_BACKTRACE = 1; 46 + 47 + meta = { 48 + description = "Automatic systemd service hardening guided by strace profiling"; 49 + homepage = "https://github.com/desbma/shh"; 50 + license = lib.licenses.gpl3Only; 51 + platforms = lib.platforms.linux; 52 + mainProgram = "shh"; 53 + maintainers = with lib.maintainers; [ 54 + erdnaxe 55 + kuflierl 56 + ]; 57 + }; 58 + }
+83
pkgs/by-name/sh/shh/pr13-profile-path-fix-strace.patch
··· 1 + commit 4d2c1556d769695770c95a982e0dcda4d70eee57 2 + Author: kuflierl <41301536+kuflierl@users.noreply.github.com> 3 + Date: Sun Apr 13 19:57:50 2025 +0200 4 + 5 + service.rs: profile path fix for strace 6 + Enable path env fixing when path env doesn't have strace to unbreak tool on unique systems and units. 7 + This fixes handling on non FHS operating systems and systemd units that define their own PATH that doesn't include strace. 8 + 9 + diff --git a/src/systemd/service.rs b/src/systemd/service.rs 10 + index 908fdf0..e9294cf 100644 11 + --- a/src/systemd/service.rs 12 + +++ b/src/systemd/service.rs 13 + @@ -7,6 +7,7 @@ use std::{ 14 + ops::RangeInclusive, 15 + path::{Path, PathBuf}, 16 + process::{Command, Stdio}, 17 + + ffi::OsString, 18 + }; 19 + 20 + use anyhow::Context as _; 21 + @@ -99,6 +100,41 @@ impl Service { 22 + ) 23 + } 24 + 25 + + // A function for locating the parent directory i.e. PATH of an executable 26 + + fn resolve_exec_path<P>(exe_name: &P, path_env: OsString) -> Option<PathBuf> 27 + + where P: AsRef<Path> + ?Sized, 28 + + { 29 + + env::split_paths(&path_env).filter_map(|dir| { 30 + + let full_path = dir.join(&exe_name); 31 + + if full_path.is_file() { 32 + + Some(dir) 33 + + } else { 34 + + None 35 + + } 36 + + }).next() 37 + + } 38 + + 39 + + // determine PATH env used for unit 40 + + pub(crate) fn get_exec_path(config_paths: &Vec<&Path>) -> anyhow::Result<String> { 41 + + let old_path_env_option = Self::config_vals("Environment", &config_paths)? 42 + + .into_iter().filter(|x| x.starts_with("\"PATH=")).last().map(|x| x.trim_matches('\"').get(5..).unwrap().to_owned()); 43 + + Ok(match old_path_env_option { 44 + + Some(path_env) => { 45 + + log::info!("Found hard coded PATH environment in unit: {path_env}"); 46 + + path_env 47 + + }, 48 + + None => { 49 + + let output = Command::new("systemd-path").arg("search-binaries-default").output()?; 50 + + if !output.status.success() { 51 + + anyhow::bail!("systemd-path invocation failed with code {:?}", output.status); 52 + + } 53 + + let default_systemd_path = output.stdout.lines().next().ok_or_else(|| anyhow::anyhow!("Unable to get global systemd default PATH"))??; 54 + + log::info!("Could not find hard coded PATH environment in unit, using systemd default: {default_systemd_path}"); 55 + + default_systemd_path 56 + + } 57 + + }) 58 + + } 59 + + 60 + /// Get systemd "exposure level" for the service (0-100). 61 + /// 100 means extremely exposed (no hardening), 0 means so sandboxed it can't do much. 62 + /// Although this is a very crude heuristic, below 40-50 is generally good. 63 + @@ -170,6 +206,20 @@ impl Service { 64 + writeln!(fragment_file, "KillMode=control-group")?; 65 + writeln!(fragment_file, "StandardOutput=journal")?; 66 + 67 + + // Modifying Env Path for strace availability if needed 68 + + let old_path_env = Self::get_exec_path(&config_paths)?; 69 + + match Self::resolve_exec_path("strace", (&old_path_env).into()) { 70 + + Some(_) => log::info!("Found strace in previous path, no correction needed"), 71 + + None => { 72 + + let path_with_strace = Self::resolve_exec_path("strace", env::var_os("PATH").unwrap()).unwrap(); 73 + + log::info!("Found strace from local PATH in {}, inserting it into unit config!", path_with_strace.display()); 74 + + let mut paths = env::split_paths(&old_path_env).collect::<Vec<_>>(); 75 + + paths.push(path_with_strace); 76 + + let new_path = env::join_paths(paths)?; 77 + + writeln!(fragment_file, "Environment=\"PATH={}\"", new_path.to_str().unwrap())?; 78 + + }, 79 + + } 80 + + 81 + // Profile data dir 82 + let mut rng = rand::rng(); 83 + let profile_data_dir = PathBuf::from(format!(