std: fix stack buffer overflow in Windows junction_point#158147
std: fix stack buffer overflow in Windows junction_point#158147devnexen wants to merge 1 commit into
Conversation
|
r? @Darksonn rustbot has assigned @Darksonn. Use Why was this reviewer chosen?The reviewer was selected based on:
|
There was a problem hiding this comment.
It sounds like this should be changed to ptr[..abs_path.len()].copy_from_slice(abs_path) or similar so that we actually perform a bounds check here.
Also, it would be really nice with a test that'd catch this (may be easier to check that the test is failing if we insert a bounds check first).
There was a problem hiding this comment.
In fact, maybe the bounds check should be entirely rewritten to
ptr.get(..abs_path.len())
.ok_or_else(|| io::const_error!(io::ErrorKind::InvalidInput, "`original` path is too long"))?
.copy_from_slice(abs_path)
This way there's no risk that the two bounds checks are not kept in sync. For instance, why is there a + 8 in the previous check? I don't understand that part.
| PathBuffer: [MaybeUninit<u16>; c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize], | ||
| } | ||
| let data_len = 12 + (abs_path.len() * 2); | ||
| if data_len > u16::MAX as usize { | ||
| if data_len + 8 > c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize { | ||
| return Err(io::const_error!(io::ErrorKind::InvalidInput, "`original` path is too long")); |
There was a problem hiding this comment.
data_len counts the number of bytes, but MAXIMUM_REPARSE_DATA_BUFFER_SIZE counts the number of u16s. This doesn't sound right.
|
Reminder, once the PR becomes ready for a review, use |
This comment has been minimized.
This comment has been minimized.
|
@rustbot ready |
|
|
||
| #[test] | ||
| #[cfg(windows)] | ||
| fn junction_point_overlong_path() { |
There was a problem hiding this comment.
I'd like to see this regression test fail without the change. Do you mind opening a second temporary PR containing just the test so that we can run it through CI? We can close it again when we've confirmed the regression test catches the bug.
| PathBuffer: [MaybeUninit::uninit(); c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize], | ||
| PathBuffer: [MaybeUninit::uninit(); c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize / 2], | ||
| }; | ||
| // The path is followed by its null terminator and the (empty) print name's | ||
| // null terminator, so the buffer must hold two extra `u16`s. This single | ||
| // bounds check keeps the buffer, the copy, and `data_len` below in sync; if | ||
| // the path doesn't fit, fail rather than overflow the buffer. | ||
| let Some(ptr) = header.PathBuffer.get_mut(..abs_path.len() + 2) else { | ||
| return Err(io::const_error!(io::ErrorKind::InvalidInput, "`original` path is too long")); | ||
| }; | ||
| ptr[..abs_path.len()].write_copy_of_slice(&abs_path); |
There was a problem hiding this comment.
This buffer is uninitialized. Don't you need to set path[abs_path.len()] and path[abs_path.len()+1] to zero?
There was a problem hiding this comment.
The path has an explicit length so it doesn't need to be null terminated at all. I'm uncertain where that idea came from but it's probably just a misunderstanding? I mean, I guess there's no harm in writing a null but if so it shouldn't be part of the length.
|
To give some context here, this was originally a hacky function used only in tests. It's currently publicly exposed as a nightly-only API but it's not considered ready for stabilisation. E.g. 16kb is way too much for a stack buffer (though admittedly using a stack buffer for shorter paths would be useful). |
| PathBuffer: [MaybeUninit<u16>; c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize], | ||
| // `MAXIMUM_REPARSE_DATA_BUFFER_SIZE` is a size in bytes; halve it for a | ||
| // count of `u16`s (the `readlink` path uses it as a byte buffer). | ||
| PathBuffer: [MaybeUninit<u16>; c::MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize / 2], |
There was a problem hiding this comment.
This doesn't sound right at all. What makes you say MAXIMUM_REPARSE_DATA_BUFFER_SIZE is in bytes?
There was a problem hiding this comment.
Huh, apparently it is.
The protocol docs make no mention of a maximum size, only that it's a 16 bit unsigned integer (hence my surprise at 16kb being a limit). But the public headers for the kernel API do have MAXIMUM_REPARSE_DATA_BUFFER_SIZE in bytes of 16kb.
There was a problem hiding this comment.
Should it be changed to an u8 array as well?
There was a problem hiding this comment.
I would say no. It's the mount-point WCHAR path, and u16 keeps the copy a clean write_copy_of_slice(&abs_path).
d842451 to
28748e5
Compare
Temporary CI-verification test for #158147. Without the fix, a >16 KiB junction target passes the old `> u16::MAX` length check yet overflows the inline reparse stack buffer. This test must fail on master and pass once the fix lands.
|
@rustbot ready |
| // Size of the reparse data after the 8-byte ReparseTag/ReparseDataLength/ | ||
| // Reserved header: the four name offset/length `u16` fields (8 bytes) plus | ||
| // the path. | ||
| let data_len = 8 + abs_path.len() * 2; |
There was a problem hiding this comment.
Isn't it 16 bytes?
- ReparseTag is 4 bytes
- ReparseDataLength is 2 bytes
- Reserved is 2 bytes
- SubstituteNameOffset is 2 bytes
- SubstituteNameLength is 2 bytes
- PrintNameOffset is 2 bytes
- PrintNameLength is 2 bytes
There was a problem hiding this comment.
Right, it's 16 bytes. Switched to offset_of! so both lengths derive from the struct.
The guard checked `data_len > u16::MAX`, allowing paths far larger than `PathBuffer` (a fixed 16384-element array), which the subsequent single `copy_from` then overflows. Bound against `MAXIMUM_REPARSE_DATA_BUFFER_SIZE` plus header instead, matching the kernel's limit.
28748e5 to
b7af0d1
Compare
|
@rustbot ready |
There was a problem hiding this comment.
This looks okay to me. @ChrisDenton do you have any comments before I merge it?
View all comments
The guard checked
data_len > u16::MAX, allowing paths far larger thanPathBuffer(a fixed 16384-element array), which the subsequent singlecopy_fromthen overflows. Bound againstMAXIMUM_REPARSE_DATA_BUFFER_SIZEplus header instead, matching the kernel's limit.