Skip to content

Commit c13eef2

Browse files
committed
Distrust path length
1 parent d93faf8 commit c13eef2

File tree

7 files changed

+41
-21
lines changed

7 files changed

+41
-21
lines changed

crates/uv-trampoline/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ The intended use is:
3030
integer.
3131
* At the very end, write the magic number `UVUV` in bytes.
3232

33+
| `launcher.exe` |
34+
|:---------------------------:|
35+
| `<zipped python script>` |
36+
| `<path to python.exe>` |
37+
| `<len(path to python.exe)>` |
38+
| `<b'U', b'V', b'U', b'V'>` |
39+
3340
Then when you run `python` on the `.exe`, it will see the `.zip` trailer at the
3441
end of the `.exe`, and automagically look inside to find and execute
3542
`__main__.py`. Easy-peasy.

crates/uv-trampoline/src/bounce.rs

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::{c, eprintln, format};
2828

2929
const MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V'];
3030
const PATH_LEN_SIZE: usize = mem::size_of::<u32>();
31+
const MAX_PATH_LEN: u32 = 2 * 1024 * 1024;
3132

3233
fn getenv(name: &CStr) -> Option<CString> {
3334
unsafe {
@@ -145,7 +146,7 @@ fn find_python_exe(executable_name: &CStr) -> CString {
145146
let file_handle = expect_result(
146147
unsafe {
147148
CreateFileA(
148-
executable_name.to_bytes()[..0].as_ptr(),
149+
executable_name.as_ptr() as _,
149150
GENERIC_READ,
150151
FILE_SHARE_READ,
151152
null(),
@@ -179,15 +180,18 @@ fn find_python_exe(executable_name: &CStr) -> CString {
179180

180181
// Start with a size of 1024 bytes which should be enough for most paths but avoids reading the
181182
// entire file.
182-
let mut buffer: Vec<u8> = vec![0; 1024];
183+
let mut buffer: Vec<u8> = Vec::new();
184+
let mut bytes_to_read = 1024.min(u32::try_from(file_size).unwrap_or(u32::MAX));
185+
183186
let path = loop {
184-
let bytes_to_read = u32::try_from(buffer.capacity()).unwrap_or(u32::MAX);
187+
// SAFETY: Casting to usize is safe because we only support 64bit systems where usize is guaranteed to be larger than u32.
188+
buffer.resize(bytes_to_read as usize, 0);
185189

186190
expect_result(
187191
unsafe {
188192
SetFilePointerEx(
189193
file_handle,
190-
file_size.saturating_sub(bytes_to_read as i64),
194+
file_size - i64::from(bytes_to_read),
191195
null_mut(),
192196
FILE_BEGIN,
193197
)
@@ -225,11 +229,18 @@ fn find_python_exe(executable_name: &CStr) -> CString {
225229

226230
let path_len = match buffer.get(buffer.len() - PATH_LEN_SIZE..) {
227231
Some(path_len) => {
228-
// We only support 64bit systems where usize is guaranteed to be larger than u32.
229-
u32::from_le_bytes(path_len.try_into().unwrap_or_else(|_| {
232+
let path_len = u32::from_le_bytes(path_len.try_into().unwrap_or_else(|_| {
230233
eprintln!("Slice length is not equal to 4 bytes");
231234
exit_with_status(1)
232-
})) as usize
235+
}));
236+
237+
if path_len > MAX_PATH_LEN {
238+
eprintln!("Only paths with a length up to 2MBs are supported but the python path has a length of {}.", path_len);
239+
exit_with_status(1);
240+
}
241+
242+
// SAFETY: path len is guaranteed to be less than 2MB
243+
path_len as usize
233244
}
234245
None => {
235246
eprintln!("Python executable length missing. Did you write the length of the path to the Python executable before the Magic number?");
@@ -249,16 +260,14 @@ fn find_python_exe(executable_name: &CStr) -> CString {
249260
exit_with_status(1)
250261
});
251262
} else {
252-
let new_len = path_len
253-
.saturating_add(MAGIC_NUMBER.len())
254-
.saturating_add(PATH_LEN_SIZE);
263+
// SAFETY: Casting to u32 is safe because `path_len` is guaranteed to be less than 2MB,
264+
// MAGIC_NUMBER is 4 bytes and PATH_LEN_SIZE is 4 bytes.
265+
bytes_to_read = (path_len + MAGIC_NUMBER.len() + PATH_LEN_SIZE) as u32;
255266

256-
if path_len >= usize::try_from(file_size).unwrap_or(usize::MAX) {
257-
eprintln!("The length of the path to the Python executable is larger than the file size. Did you write the length of the path to the Python executable before the Magic number?");
267+
if i64::from(bytes_to_read) > file_size {
268+
eprintln!("The length of the python executable path exceeds the file size. Verify that the path length is appended to the end of the launcher script as a u32 in little endian.");
258269
exit_with_status(1);
259270
}
260-
261-
buffer.resize(new_len, 0);
262271
}
263272
};
264273

@@ -498,9 +507,9 @@ pub fn bounce(is_gui: bool) -> ! {
498507
/// Prints the passed error message if the `actual_result` is equal to `error_code` and exits the process with status 1.
499508
#[inline]
500509
fn expect_result<T, F>(actual_result: T, error_code: T, error_message: F) -> T
501-
where
502-
T: Eq,
503-
F: FnOnce() -> String,
510+
where
511+
T: Eq,
512+
F: FnOnce() -> String,
504513
{
505514
if actual_result == error_code {
506515
print_last_error_and_exit(&error_message());
@@ -550,7 +559,11 @@ fn print_last_error_and_exit(message: &str) -> ! {
550559
let reason = core::slice::from_raw_parts(msg_ptr, size as usize + 1);
551560
CStr::from_bytes_with_nul_unchecked(reason)
552561
};
553-
eprintln!("{}: {}", message, &*reason.to_string_lossy());
562+
eprintln!(
563+
"(uv internal error) {}: {}",
564+
message,
565+
&*reason.to_string_lossy()
566+
);
554567
}
555568

556569
// Note: We don't need to free the buffer here because we're going to exit anyway.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

crates/uv/tests/common/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ impl TestContext {
115115
self.assert_command(
116116
format!("import {package} as package; print(package.__version__, end='')").as_str(),
117117
)
118-
.success()
119-
.stdout(version);
118+
.success()
119+
.stdout(version);
120120
}
121121

122122
/// Generate an escaped regex pattern for the given path.
@@ -292,7 +292,7 @@ pub fn create_bin_with_executables(
292292
&Platform::current().unwrap(),
293293
&Cache::temp().unwrap(),
294294
)?
295-
.ok_or(uv_interpreter::Error::NoSuchPython(request.to_string()))?;
295+
.ok_or(uv_interpreter::Error::NoSuchPython(request.to_string()))?;
296296
let name = interpreter
297297
.sys_executable()
298298
.file_name()

0 commit comments

Comments
 (0)