Skip to content

Commit 0074393

Browse files
authored
Randomize worker script paths. (#3)
1 parent 662324a commit 0074393

1 file changed

Lines changed: 111 additions & 5 deletions

File tree

src/pool.rs

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ use crate::backend::{DecompileBackend, DecompileRequest, DecompileResult, Mode};
1313
use crate::model::WorkerMessage;
1414
use crate::progress::Progress;
1515
use anyhow::{anyhow, bail, Context, Result};
16-
use std::io::{BufRead, BufReader, Read};
16+
use std::collections::hash_map::RandomState;
17+
use std::fs::{DirBuilder, OpenOptions};
18+
use std::hash::{BuildHasher, Hasher};
19+
use std::io::{BufRead, BufReader, Read, Write};
1720
use std::path::PathBuf;
1821
use std::process::{Command, Stdio};
1922
use std::sync::atomic::{AtomicBool, Ordering};
@@ -77,18 +80,75 @@ fn kill(pid: u32) {
7780
.status();
7881
}
7982

83+
/// Random hex token drawn from `RandomState`, whose SipHash keys are seeded
84+
/// from OS entropy — unpredictable temp names without a rand dependency.
85+
fn random_token() -> String {
86+
fn draw() -> u64 {
87+
let mut hasher = RandomState::new().build_hasher();
88+
hasher.write_u64(0);
89+
hasher.finish()
90+
}
91+
format!("{:016x}{:016x}", draw(), draw())
92+
}
93+
94+
/// Create a fresh private subdirectory under the system temp dir.
95+
///
96+
/// The name is random and `create_dir` refuses to reuse an existing path, so
97+
/// we never adopt a directory another user prepared. On Unix it is created
98+
/// mode 0o700 so no other user can write into or traverse it.
99+
fn create_private_dir() -> Result<PathBuf> {
100+
let mut builder = DirBuilder::new();
101+
#[cfg(unix)]
102+
{
103+
use std::os::unix::fs::DirBuilderExt;
104+
builder.mode(0o700);
105+
}
106+
for _ in 0..16 {
107+
let dir = std::env::temp_dir().join(format!("srcdump-{}", random_token()));
108+
match builder.create(&dir) {
109+
Ok(()) => return Ok(dir),
110+
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
111+
Err(e) => {
112+
return Err(e).with_context(|| format!("creating worker dir {}", dir.display()))
113+
}
114+
}
115+
}
116+
bail!(
117+
"could not create a private worker dir under {}",
118+
std::env::temp_dir().display()
119+
);
120+
}
121+
80122
pub struct ProcessPoolBackend {
123+
worker_dir: PathBuf,
81124
worker_script: PathBuf,
82125
}
83126

84127
impl ProcessPoolBackend {
85-
/// Materialize the embedded worker script to a temp file for this process.
128+
/// Materialize the embedded worker script to a randomly-named file inside
129+
/// a private (user-only) temp subdirectory for this process.
86130
pub fn new() -> Result<Self> {
87-
let mut path = std::env::temp_dir();
88-
path.push(format!("srcdump_worker_{}.py", std::process::id()));
89-
std::fs::write(&path, WORKER_SOURCE)
131+
let dir = create_private_dir()?;
132+
let path = dir.join(format!("worker-{}.py", random_token()));
133+
// Unlink anything already occupying the name, then create exclusively
134+
// (`create_new` never follows or truncates an existing file), so a
135+
// pre-planted file or symlink can neither survive nor be written
136+
// through.
137+
let _ = std::fs::remove_file(&path);
138+
let mut opts = OpenOptions::new();
139+
opts.write(true).create_new(true);
140+
#[cfg(unix)]
141+
{
142+
use std::os::unix::fs::OpenOptionsExt;
143+
opts.mode(0o600);
144+
}
145+
let mut file = opts
146+
.open(&path)
147+
.with_context(|| format!("creating worker script {}", path.display()))?;
148+
file.write_all(WORKER_SOURCE.as_bytes())
90149
.with_context(|| format!("writing worker script to {}", path.display()))?;
91150
Ok(Self {
151+
worker_dir: dir,
92152
worker_script: path,
93153
})
94154
}
@@ -97,6 +157,7 @@ impl ProcessPoolBackend {
97157
impl Drop for ProcessPoolBackend {
98158
fn drop(&mut self) {
99159
let _ = std::fs::remove_file(&self.worker_script);
160+
let _ = std::fs::remove_dir(&self.worker_dir);
100161
}
101162
}
102163

@@ -361,3 +422,48 @@ fn tail_lines(s: &str, n: usize) -> String {
361422
let start = lines.len().saturating_sub(n);
362423
lines[start..].join("\n")
363424
}
425+
426+
#[cfg(test)]
427+
mod tests {
428+
use super::*;
429+
430+
#[test]
431+
fn worker_script_lives_in_private_random_dir() {
432+
let backend = ProcessPoolBackend::new().unwrap();
433+
434+
assert!(backend.worker_script.is_file());
435+
assert_eq!(backend.worker_script.parent(), Some(&*backend.worker_dir));
436+
assert_eq!(backend.worker_dir.parent(), Some(&*std::env::temp_dir()));
437+
assert_eq!(
438+
std::fs::read_to_string(&backend.worker_script).unwrap(),
439+
WORKER_SOURCE
440+
);
441+
442+
#[cfg(unix)]
443+
{
444+
use std::os::unix::fs::PermissionsExt;
445+
let dir_mode = std::fs::metadata(&backend.worker_dir)
446+
.unwrap()
447+
.permissions()
448+
.mode();
449+
assert_eq!(dir_mode & 0o777, 0o700, "worker dir must be user-only");
450+
let file_mode = std::fs::metadata(&backend.worker_script)
451+
.unwrap()
452+
.permissions()
453+
.mode();
454+
assert_eq!(file_mode & 0o777, 0o600, "worker script must be user-only");
455+
}
456+
457+
// Two backends must never collide on paths.
458+
let other = ProcessPoolBackend::new().unwrap();
459+
assert_ne!(backend.worker_dir, other.worker_dir);
460+
assert_ne!(backend.worker_script, other.worker_script);
461+
462+
// Drop removes both the script and its private directory.
463+
let (dir, script) = (backend.worker_dir.clone(), backend.worker_script.clone());
464+
drop(backend);
465+
assert!(!script.exists());
466+
assert!(!dir.exists());
467+
drop(other);
468+
}
469+
}

0 commit comments

Comments
 (0)