Skip to content

Commit 23b354d

Browse files
committed
split: harden output open path against TOCTOU target swaps
1 parent ef8e45c commit 23b354d

3 files changed

Lines changed: 163 additions & 34 deletions

File tree

src/uu/split/src/platform/unix.rs

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::io::{BufWriter, Error, Result};
88
use std::io::{ErrorKind, Write};
99
use std::path::Path;
1010
use std::process::{Child, Command, Stdio};
11+
use uucore::display::Quotable;
1112
use uucore::error::USimpleError;
1213
use uucore::fs;
1314
use uucore::fs::FileInformation;
@@ -127,36 +128,32 @@ impl Drop for FilterWriter {
127128
/// Instantiate either a file writer or a "write to shell process's stdin" writer
128129
pub fn instantiate_current_writer(
129130
filter: Option<&str>,
131+
input: &OsStr,
130132
filename: &str,
131133
is_new: bool,
132134
) -> Result<BufWriter<Box<dyn Write>>> {
133135
match filter {
134136
None => {
135137
let file = if is_new {
136-
// create new file
137-
std::fs::OpenOptions::new()
138-
.write(true)
139-
.create(true)
140-
.truncate(true)
141-
.open(Path::new(&filename))
142-
.map_err(|e| match e.kind() {
143-
ErrorKind::IsADirectory => Error::other(
144-
translate!("split-error-is-a-directory", "dir" => filename),
145-
),
146-
_ => Error::other(
147-
translate!("split-error-unable-to-open-file", "file" => filename),
148-
),
149-
})?
138+
create_or_truncate_output_file(input, filename)?
150139
} else {
151140
// re-open file that we previously created to append to it
152-
std::fs::OpenOptions::new()
141+
let file = std::fs::OpenOptions::new()
153142
.append(true)
154143
.open(Path::new(&filename))
155144
.map_err(|_| {
156145
Error::other(
157146
translate!("split-error-unable-to-reopen-file", "file" => filename),
158147
)
159-
})?
148+
})?;
149+
150+
if input_and_output_refer_to_same_file(input, &file) {
151+
return Err(Error::other(
152+
translate!("split-error-would-overwrite-input", "file" => filename.quote()),
153+
));
154+
}
155+
156+
file
160157
};
161158
Ok(BufWriter::new(Box::new(file) as Box<dyn Write>))
162159
}
@@ -167,6 +164,52 @@ pub fn instantiate_current_writer(
167164
}
168165
}
169166

167+
fn create_or_truncate_output_file(input: &OsStr, filename: &str) -> Result<std::fs::File> {
168+
match std::fs::OpenOptions::new()
169+
.write(true)
170+
.create_new(true)
171+
.open(Path::new(filename))
172+
{
173+
Ok(file) => Ok(file),
174+
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
175+
let file = std::fs::OpenOptions::new()
176+
.write(true)
177+
.open(Path::new(filename))
178+
.map_err(|err| open_file_error(filename, err.kind()))?;
179+
180+
if input_and_output_refer_to_same_file(input, &file) {
181+
return Err(Error::other(
182+
translate!("split-error-would-overwrite-input", "file" => filename.quote()),
183+
));
184+
}
185+
186+
file.set_len(0)
187+
.map_err(|err| open_file_error(filename, err.kind()))?;
188+
Ok(file)
189+
}
190+
Err(e) => Err(open_file_error(filename, e.kind())),
191+
}
192+
}
193+
194+
fn open_file_error(filename: &str, kind: ErrorKind) -> Error {
195+
match kind {
196+
ErrorKind::IsADirectory => {
197+
Error::other(translate!("split-error-is-a-directory", "dir" => filename))
198+
}
199+
_ => Error::other(translate!("split-error-unable-to-open-file", "file" => filename)),
200+
}
201+
}
202+
203+
fn input_and_output_refer_to_same_file(input: &OsStr, output: &std::fs::File) -> bool {
204+
let input_info = if input == "-" {
205+
FileInformation::from_file(&std::io::stdin())
206+
} else {
207+
FileInformation::from_path(Path::new(input), true)
208+
};
209+
210+
fs::infos_refer_to_same_file(input_info, FileInformation::from_file(output))
211+
}
212+
170213
pub fn paths_refer_to_same_file(p1: &OsStr, p2: &OsStr) -> bool {
171214
// We have to take symlinks and relative paths into account.
172215
let p1 = if p1 == "-" {
@@ -176,3 +219,45 @@ pub fn paths_refer_to_same_file(p1: &OsStr, p2: &OsStr) -> bool {
176219
};
177220
fs::infos_refer_to_same_file(p1, FileInformation::from_path(Path::new(p2), true))
178221
}
222+
223+
#[cfg(test)]
224+
mod tests {
225+
use super::instantiate_current_writer;
226+
use std::fs;
227+
use std::os::unix::fs::symlink;
228+
use std::time::{SystemTime, UNIX_EPOCH};
229+
230+
#[test]
231+
fn reopened_writer_rejects_symlink_swapped_to_input() {
232+
let tmp = std::env::temp_dir().join(format!(
233+
"uutils-split-{}-{}",
234+
std::process::id(),
235+
SystemTime::now()
236+
.duration_since(UNIX_EPOCH)
237+
.expect("system time before unix epoch")
238+
.as_nanos()
239+
));
240+
fs::create_dir_all(&tmp).expect("failed to create temp dir");
241+
242+
let input = tmp.join("input.txt");
243+
let output = tmp.join("xaa");
244+
fs::write(&input, b"input\n").expect("failed to write input");
245+
symlink(&input, &output).expect("failed to create output symlink");
246+
247+
let Err(err) = instantiate_current_writer(
248+
None,
249+
input.as_os_str(),
250+
output.to_str().expect("non-utf8 path"),
251+
false,
252+
) else {
253+
panic!("re-opened writer should reject swapped symlink");
254+
};
255+
assert!(
256+
err.to_string()
257+
.contains("split-error-would-overwrite-input"),
258+
"unexpected error: {err}"
259+
);
260+
261+
fs::remove_dir_all(&tmp).expect("failed to cleanup temp dir");
262+
}
263+
}

src/uu/split/src/platform/windows.rs

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::ffi::OsStr;
66
use std::io::{BufWriter, Error, Result};
77
use std::io::{ErrorKind, Write};
88
use std::path::Path;
9+
use uucore::display::Quotable;
910
use uucore::fs;
1011
use uucore::translate;
1112

@@ -15,36 +16,77 @@ use uucore::translate;
1516
/// a file writer
1617
pub fn instantiate_current_writer(
1718
_filter: Option<&str>,
19+
input: &OsStr,
1820
filename: &str,
1921
is_new: bool,
2022
) -> Result<BufWriter<Box<dyn Write>>> {
2123
let file = if is_new {
22-
// create new file
23-
std::fs::OpenOptions::new()
24-
.write(true)
25-
.create(true)
26-
.truncate(true)
27-
.open(Path::new(&filename))
28-
.map_err(|e| match e.kind() {
29-
ErrorKind::IsADirectory => {
30-
Error::other(translate!("split-error-is-a-directory", "dir" => filename))
31-
}
32-
_ => {
33-
Error::other(translate!("split-error-unable-to-open-file", "file" => filename))
34-
}
35-
})?
24+
create_or_truncate_output_file(input, filename)?
3625
} else {
3726
// re-open file that we previously created to append to it
38-
std::fs::OpenOptions::new()
27+
let file = std::fs::OpenOptions::new()
3928
.append(true)
4029
.open(Path::new(&filename))
4130
.map_err(|_| {
4231
Error::other(translate!("split-error-unable-to-reopen-file", "file" => filename))
43-
})?
32+
})?;
33+
34+
if input_and_output_refer_to_same_file(input, &file) {
35+
return Err(Error::other(
36+
translate!("split-error-would-overwrite-input", "file" => filename.quote()),
37+
));
38+
}
39+
40+
file
4441
};
4542
Ok(BufWriter::new(Box::new(file) as Box<dyn Write>))
4643
}
4744

45+
fn create_or_truncate_output_file(input: &OsStr, filename: &str) -> Result<std::fs::File> {
46+
match std::fs::OpenOptions::new()
47+
.write(true)
48+
.create_new(true)
49+
.open(Path::new(filename))
50+
{
51+
Ok(file) => Ok(file),
52+
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
53+
let file = std::fs::OpenOptions::new()
54+
.write(true)
55+
.open(Path::new(filename))
56+
.map_err(|err| open_file_error(filename, err.kind()))?;
57+
58+
if input_and_output_refer_to_same_file(input, &file) {
59+
return Err(Error::other(
60+
translate!("split-error-would-overwrite-input", "file" => filename.quote()),
61+
));
62+
}
63+
64+
file.set_len(0)
65+
.map_err(|err| open_file_error(filename, err.kind()))?;
66+
Ok(file)
67+
}
68+
Err(e) => Err(open_file_error(filename, e.kind())),
69+
}
70+
}
71+
72+
fn open_file_error(filename: &str, kind: ErrorKind) -> Error {
73+
match kind {
74+
ErrorKind::IsADirectory => {
75+
Error::other(translate!("split-error-is-a-directory", "dir" => filename))
76+
}
77+
_ => Error::other(translate!("split-error-unable-to-open-file", "file" => filename)),
78+
}
79+
}
80+
81+
fn input_and_output_refer_to_same_file(input: &OsStr, output: &std::fs::File) -> bool {
82+
let input_info = if input == "-" {
83+
fs::FileInformation::from_file(&std::io::stdin())
84+
} else {
85+
fs::FileInformation::from_path(Path::new(input), true)
86+
};
87+
88+
fs::infos_refer_to_same_file(input_info, fs::FileInformation::from_file(output))
89+
}
4890
pub fn paths_refer_to_same_file(p1: &OsStr, p2: &OsStr) -> bool {
4991
// Windows doesn't support many of the unix ways of paths being equals
5092
fs::paths_refer_to_same_file(Path::new(p1), Path::new(p2), true)

src/uu/split/src/split.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,13 +543,15 @@ impl Settings {
543543
filename: &str,
544544
is_new: bool,
545545
) -> io::Result<BufWriter<Box<dyn Write>>> {
546-
if platform::paths_refer_to_same_file(&self.input, filename.as_ref()) {
546+
if self.filter.is_some()
547+
&& platform::paths_refer_to_same_file(&self.input, filename.as_ref())
548+
{
547549
return Err(io::Error::other(
548550
translate!("split-error-would-overwrite-input", "file" => filename.quote()),
549551
));
550552
}
551553

552-
platform::instantiate_current_writer(self.filter.as_deref(), filename, is_new)
554+
platform::instantiate_current_writer(self.filter.as_deref(), &self.input, filename, is_new)
553555
}
554556
}
555557

0 commit comments

Comments
 (0)