-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fs: Don't copy d_name from struct dirent #93459
Conversation
r? @kennytm (rust-highfive has picked a reviewer for you, use r? to override) |
I added a test, and was able to reproduce the failure with $ RUSTFLAGS_BOOTSTRAP="-Z sanitizer=address" ./x.py test --stage 0 library/std --test-args read_large_dir and confirmed that this PR fixes it. |
4f0de2b
to
e8690d8
Compare
e8690d8
to
db00028
Compare
library/std/src/sys/unix/fs.rs
Outdated
// Only d_reclen bytes of *entry_ptr are valid, so we can't just copy the | ||
// whole thing (#93384). Instead, copy everything except the name. | ||
let entry_bytes = entry_ptr as *const u8; | ||
let entry_name = (*entry_ptr).d_name.as_ptr() as *const u8; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I suggested ptr::addr_of!
is because things like as_ptr()
require forming a reference &self
, but our issue here is that &d_name
may not be fully dereferenceable. Keeping everything as pointers should be safer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah fair, I did it this way because the code below was already doing the same thing. I'll change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out addr_of!()
isn't even enough: rust-lang/miri#1981 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, then what? Get the offset from a fully-sized local dummy, then ptr-add on the real thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. That's apparently what all the offset_of!()
crates do. Does libstd already have something like that or should I just open code it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My impression was that such use was the very purpose of addr_of!
/ &raw
, and it seems I wasn't the only one from the discussion on #64490 and #73394. But that was left to the future in RFC 2582.
It seems really unfortunate here since the pointer at hand is not entirely dangling, just incomplete at the end, beyond the pointer we want to compute. A MaybeUninit
local feels like such a waste, but should be optimized away. Maybe that can even be done in a true const
these days?
I'm not aware of any existing offset_of!
, so open-coding it seems fine, with a comment that it could use offset_of!
so we can find and update it later. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addr_of!
is a weaker from of &
, but it does nothing about *
. The code here is UB due to the requirement of *ptr
to only be used on pointers that point to actual memory (with the required size and alignment given by its type).
I would love to just entirely remove that restriction, but then we would have to change our LLVM codegen to use getelementptr
instead of getelementptr inbounds
in some situations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe that can even be done in a true const these days?
With some unstable features, it can -- see the https://github.com/Gilnaa/memoffset crate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would love to just entirely remove that restriction, but then we would have to change our LLVM codegen to use getelementptr instead of getelementptr inbounds in some situations.
This is probably worth doing, honestly. I can't imagine the use of gepi
over gep
wins us almost anything on modern hardware (IIUC it mostly just allows folding the address arithmetic into a subsequent load), and even if it does, we still would be able to use it in all cases except for with addr_of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
r? @cuviper |
The dirent returned from readdir() is only guaranteed to be valid for d_reclen bytes on common platforms. Since we copy the name separately anyway, we can copy everything except d_name into DirEntry::entry. Fixes rust-lang#93384.
db00028
to
d0c8b29
Compare
LGTM -- bumping priority since this helps everyone's CI. @bors r+ p=1 |
📌 Commit d0c8b29 has been approved by |
⌛ Testing commit d0c8b29 with merge c7a67c9e4bd271310a9b6a163aa41c82ccf863ed... |
💔 Test failed - checks-actions |
@bors retry The build was canceled after 10 minutes, though it isn't clear why. |
…askrgr Rollup of 8 pull requests Successful merges: - rust-lang#93256 (Make `join!` description more accurate) - rust-lang#93358 (Add note suggesting that predicate may be satisfied, but is not `const`) - rust-lang#93362 (Do not register infer var for GAT projection in RPIT) - rust-lang#93391 (rustdoc: remove tooltip from source link) - rust-lang#93414 (Move unstable is_{arch}_feature_detected! macros to std::arch) - rust-lang#93441 (rustdoc: load the set of in-scope traits for modules with no docstring) - rust-lang#93459 (fs: Don't copy d_name from struct dirent) - rust-lang#93463 (Rename _args -> args in format_args expansion) Failed merges: r? `@ghost` `@rustbot` modify labels: rollup
ptr::addr_of!((*ptr).field) still requires ptr to point to an appropriate allocation for its type. Since the pointer returned by readdir() can be smaller than sizeof(struct dirent), we need to entirely avoid dereferencing it as that type. Link: rust-lang/miri#1981 (comment) Link: rust-lang#93459 (comment)
…=cuviper fs: Don't dereference a pointer to a too-small allocation ptr::addr_of!((*ptr).field) still requires ptr to point to an appropriate allocation for its type. Since the pointer returned by readdir() can be smaller than sizeof(struct dirent), we need to entirely avoid dereferencing it as that type. Link: rust-lang/miri#1981 (comment) Link: rust-lang#93459 (comment)
Eliminate 280-byte memset from ReadDir iterator This guy: https://github.com/rust-lang/rust/blob/1536ab1b383f21b38f8d49230a2aecc51daffa3d/library/std/src/sys/unix/fs.rs#L589 It turns out `libc::dirent64` is quite big—https://docs.rs/libc/0.2.135/libc/struct.dirent64.html. In rust-lang#103135 this memset accounted for 0.9% of the runtime of iterating a big directory. Almost none of the big zeroed value is ever used. We memcpy a tiny prefix (19 bytes) into it, and then read just 9 bytes (`d_ino` and `d_type`) back out. We can read exactly those 9 bytes we need directly from the original entry_ptr instead. ## History This code got added in rust-lang#93459 and tweaked in rust-lang#94272 and rust-lang#94750. Prior to rust-lang#93459, there was no memset but a full 280 bytes were being copied from the entry_ptr. <table><tr><td>copy 280 bytes</td></tr></table> This was not legal because not all of those bytes might be initialized, or even allocated, depending on the length of the directory entry's name, leading to a segfault. That PR fixed the segfault by creating a new zeroed dirent64 and copying just the guaranteed initialized prefix into it. <table><tr><td>memset 280 bytes</td><td>copy 19 bytes</td></tr></table> However this was still buggy because it used `addr_of!((*entry_ptr).d_name)`, which is considered UB by Miri in the case that the full extent of entry_ptr is not in bounds of the same allocation. (Arguably this shouldn't be a requirement, but here we are.) The UB got fixed by rust-lang#94272 by replacing `addr_of` with some pointer manipulation based on `offset_from`, but still fundamentally the same operation. <table><tr><td>memset 280 bytes</td><td>copy 19 bytes</td></tr></table> Then rust-lang#94750 noticed that only 9 of those 19 bytes were even being used, so we could pick out only those 9 to put in the ReadDir value. <table><tr><td>memset 280 bytes</td><td>copy 19 bytes</td><td>copy 9 bytes</td></tr></table> After my PR we just grab the 9 needed bytes directly from entry_ptr. <table><tr><td>copy 9 bytes</td></tr></table> The resulting code is more complex but I believe still worthwhile to land for the following reason. This is an extremely straightforward thing to accomplish in C and clearly libc assumes that; literally just `entry_ptr->d_name`. The extra work in comparison to accomplish it in Rust is not an example of any actual safety being provided by Rust. I believe it's useful to have uncovered that and think about what could be done in the standard library or language to support this obvious operation better. ## References - https://man7.org/linux/man-pages/man3/readdir.3.html
Eliminate 280-byte memset from ReadDir iterator This guy: https://github.com/rust-lang/rust/blob/1536ab1b383f21b38f8d49230a2aecc51daffa3d/library/std/src/sys/unix/fs.rs#L589 It turns out `libc::dirent64` is quite big—https://docs.rs/libc/0.2.135/libc/struct.dirent64.html. In rust-lang#103135 this memset accounted for 0.9% of the runtime of iterating a big directory. Almost none of the big zeroed value is ever used. We memcpy a tiny prefix (19 bytes) into it, and then read just 9 bytes (`d_ino` and `d_type`) back out. We can read exactly those 9 bytes we need directly from the original entry_ptr instead. ## History This code got added in rust-lang#93459 and tweaked in rust-lang#94272 and rust-lang#94750. Prior to rust-lang#93459, there was no memset but a full 280 bytes were being copied from the entry_ptr. <table><tr><td>copy 280 bytes</td></tr></table> This was not legal because not all of those bytes might be initialized, or even allocated, depending on the length of the directory entry's name, leading to a segfault. That PR fixed the segfault by creating a new zeroed dirent64 and copying just the guaranteed initialized prefix into it. <table><tr><td>memset 280 bytes</td><td>copy 19 bytes</td></tr></table> However this was still buggy because it used `addr_of!((*entry_ptr).d_name)`, which is considered UB by Miri in the case that the full extent of entry_ptr is not in bounds of the same allocation. (Arguably this shouldn't be a requirement, but here we are.) The UB got fixed by rust-lang#94272 by replacing `addr_of` with some pointer manipulation based on `offset_from`, but still fundamentally the same operation. <table><tr><td>memset 280 bytes</td><td>copy 19 bytes</td></tr></table> Then rust-lang#94750 noticed that only 9 of those 19 bytes were even being used, so we could pick out only those 9 to put in the ReadDir value. <table><tr><td>memset 280 bytes</td><td>copy 19 bytes</td><td>copy 9 bytes</td></tr></table> After my PR we just grab the 9 needed bytes directly from entry_ptr. <table><tr><td>copy 9 bytes</td></tr></table> The resulting code is more complex but I believe still worthwhile to land for the following reason. This is an extremely straightforward thing to accomplish in C and clearly libc assumes that; literally just `entry_ptr->d_name`. The extra work in comparison to accomplish it in Rust is not an example of any actual safety being provided by Rust. I believe it's useful to have uncovered that and think about what could be done in the standard library or language to support this obvious operation better. ## References - https://man7.org/linux/man-pages/man3/readdir.3.html
The dirent returned from readdir() is only guaranteed to be valid for
d_reclen bytes on common platforms. Since we copy the name separately
anyway, we can copy everything except d_name into DirEntry::entry.
Fixes #93384.