Skip to content

Commit 33f4efe

Browse files
Merge #1231
1231: Add support for reading symlinks longer than `PATH_MAX` to `readlink` and `readlinkat` r=asomers a=SolraBizna This is in response to issue #1178. The new logic uses the following approach. - At any time, if `readlink` returns an error, or a value ≥ 0 and < (not ≤!) the buffer size, we're done. - Attempt to `readlink` into a `PATH_MAX` sized buffer. (This will almost always succeed, and saves a system call over calling `lstat` first.) - Try to `lstat` the link. If it succeeds and returns a sane value, allocate the buffer to be that large plus one byte. Otherwise, allocate the buffer to be `PATH_MAX.max(128) << 1` bytes. - Repeatedly attempt to `readlink`. Any time its result is ≥ (not >!) the buffer size, double the buffer size and try again. While testing this, I discovered that ext4 doesn't allow creation of a symlink > 4095 (Linux's `PATH_MAX` minus one) bytes long. This is in spite of Linux happily allowing paths in other contexts to be longer than this—including on ext4! This was probably instated to avoid breaking programs that assume `PATH_MAX` will always be enough, but ironically hindered my attempt to test support for *not* assuming. I tested the code using an artificially small `PATH_MAX` and (separately) a wired-to-fail `lstat`. `strace` showed the code behaving precisely as expected. Unfortunately, I can't add an automatic test for this. Other changes made by this PR: - `wrap_readlink_result` now calls `shrink_to_fit` on the buffer before returning, potentially reclaiming kilobytes of memory per call. This could be very important if the returned buffer is long-lived. - `readlink` and `readlink_at` now both call an `inner_readlink` function that contains the bulk of the logic, avoiding copy-pasting of code. (This is much more important now that the logic is more than a few lines long.) Notably, this PR does *not* add support for systems that don't define `PATH_MAX` at all. As far as I know, I don't have access to any POSIX-ish OS that doesn't have `PATH_MAX`, and I suspect it would have other compatibility issues with `nix` anyway. Co-authored-by: Solra Bizna <[email protected]>
2 parents 095b5be + dfee424 commit 33f4efe

File tree

2 files changed

+67
-19
lines changed

2 files changed

+67
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4545
when a group or user lookup required a buffer larger than
4646
16KB. (#[1198](https://github.com/nix-rust/nix/pull/1198))
4747
- Fixed unaligned casting of `cmsg_data` to `af_alg_iv` (#[1206](https://github.com/nix-rust/nix/pull/1206))
48+
- Fixed `readlink`/`readlinkat` when reading symlinks longer than `PATH_MAX` (#[1231](https://github.com/nix-rust/nix/pull/1231))
4849

4950
### Removed
5051

src/fcntl.rs

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -180,33 +180,80 @@ pub fn renameat<P1: ?Sized + NixPath, P2: ?Sized + NixPath>(old_dirfd: Option<Ra
180180
Errno::result(res).map(drop)
181181
}
182182

183-
fn wrap_readlink_result(v: &mut Vec<u8>, res: ssize_t) -> Result<OsString> {
184-
match Errno::result(res) {
185-
Err(err) => Err(err),
186-
Ok(len) => {
187-
unsafe { v.set_len(len as usize) }
188-
Ok(OsString::from_vec(v.to_vec()))
183+
fn wrap_readlink_result(mut v: Vec<u8>, len: ssize_t) -> Result<OsString> {
184+
unsafe { v.set_len(len as usize) }
185+
v.shrink_to_fit();
186+
Ok(OsString::from_vec(v.to_vec()))
187+
}
188+
189+
fn readlink_maybe_at<P: ?Sized + NixPath>(dirfd: Option<RawFd>, path: &P,
190+
v: &mut Vec<u8>)
191+
-> Result<libc::ssize_t> {
192+
path.with_nix_path(|cstr| {
193+
unsafe {
194+
match dirfd {
195+
Some(dirfd) => libc::readlinkat(dirfd, cstr.as_ptr(),
196+
v.as_mut_ptr() as *mut c_char,
197+
v.capacity() as size_t),
198+
None => libc::readlink(cstr.as_ptr(),
199+
v.as_mut_ptr() as *mut c_char,
200+
v.capacity() as size_t),
201+
}
189202
}
190-
}
203+
})
191204
}
192205

193-
pub fn readlink<P: ?Sized + NixPath>(path: &P) -> Result<OsString> {
206+
fn inner_readlink<P: ?Sized + NixPath>(dirfd: Option<RawFd>, path: &P)
207+
-> Result<OsString> {
194208
let mut v = Vec::with_capacity(libc::PATH_MAX as usize);
195-
let res = path.with_nix_path(|cstr| {
196-
unsafe { libc::readlink(cstr.as_ptr(), v.as_mut_ptr() as *mut c_char, v.capacity() as size_t) }
197-
})?;
198-
199-
wrap_readlink_result(&mut v, res)
209+
// simple case: result is strictly less than `PATH_MAX`
210+
let res = readlink_maybe_at(dirfd, path, &mut v)?;
211+
let len = Errno::result(res)?;
212+
debug_assert!(len >= 0);
213+
if (len as usize) < v.capacity() {
214+
return wrap_readlink_result(v, res);
215+
}
216+
// Uh oh, the result is too long...
217+
// Let's try to ask lstat how many bytes to allocate.
218+
let reported_size = super::sys::stat::lstat(path)
219+
.and_then(|x| Ok(x.st_size)).unwrap_or(0);
220+
let mut try_size = if reported_size > 0 {
221+
// Note: even if `lstat`'s apparently valid answer turns out to be
222+
// wrong, we will still read the full symlink no matter what.
223+
reported_size as usize + 1
224+
} else {
225+
// If lstat doesn't cooperate, or reports an error, be a little less
226+
// precise.
227+
(libc::PATH_MAX as usize).max(128) << 1
228+
};
229+
loop {
230+
v.reserve_exact(try_size);
231+
let res = readlink_maybe_at(dirfd, path, &mut v)?;
232+
let len = Errno::result(res)?;
233+
debug_assert!(len >= 0);
234+
if (len as usize) < v.capacity() {
235+
break wrap_readlink_result(v, res);
236+
}
237+
else {
238+
// Ugh! Still not big enough!
239+
match try_size.checked_shl(1) {
240+
Some(next_size) => try_size = next_size,
241+
// It's absurd that this would happen, but handle it sanely
242+
// anyway.
243+
None => break Err(super::Error::Sys(Errno::ENAMETOOLONG))
244+
}
245+
}
246+
}
200247
}
201248

249+
pub fn readlink<P: ?Sized + NixPath>(path: &P) -> Result<OsString> {
250+
inner_readlink(None, path)
251+
}
202252

203-
pub fn readlinkat<P: ?Sized + NixPath>(dirfd: RawFd, path: &P) -> Result<OsString> {
204-
let mut v = Vec::with_capacity(libc::PATH_MAX as usize);
205-
let res = path.with_nix_path(|cstr| {
206-
unsafe { libc::readlinkat(dirfd, cstr.as_ptr(), v.as_mut_ptr() as *mut c_char, v.capacity() as size_t) }
207-
})?;
208253

209-
wrap_readlink_result(&mut v, res)
254+
pub fn readlinkat<P: ?Sized + NixPath>(dirfd: RawFd, path: &P)
255+
-> Result<OsString> {
256+
inner_readlink(Some(dirfd), path)
210257
}
211258

212259
/// Computes the raw fd consumed by a function of the form `*at`.

0 commit comments

Comments
 (0)