Skip to content

Commit 8b87324

Browse files
committed
work around linux not honoring write_at for O_APPEND files
requires a kernel >= 6.9
1 parent 7ba34c7 commit 8b87324

File tree

4 files changed

+102
-5
lines changed

4 files changed

+102
-5
lines changed

library/std/src/fs/tests.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,23 @@ fn file_test_io_read_write_at() {
490490
check!(fs::remove_file(&filename));
491491
}
492492

493+
#[test]
494+
#[cfg(unix)]
495+
fn file_test_append_write_at() {
496+
use crate::os::unix::fs::FileExt;
497+
498+
let tmpdir = tmpdir();
499+
let filename = tmpdir.join("file_test_append_write_at.txt");
500+
let msg = b"it's not working!";
501+
check!(fs::write(&filename, &msg));
502+
// write_at should work even in in append mode
503+
let f = check!(fs::File::options().append(true).open(&filename));
504+
assert_eq!(check!(f.write_at(b" ", 5)), 3);
505+
506+
let content = check!(fs::read(&filename));
507+
assert_eq!(&content, b"it's working!");
508+
}
509+
493510
#[test]
494511
#[cfg(unix)]
495512
fn set_get_unix_permissions() {

library/std/src/os/unix/fs.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,10 @@ pub trait FileExt {
147147
///
148148
/// # Bug
149149
/// On some systems, `write_at` utilises [`pwrite64`] to write to files.
150-
/// However, this syscall has a [bug] where files opened with the `O_APPEND`
150+
/// However, on linux this syscall has a [bug] where files opened with the `O_APPEND`
151151
/// flag fail to respect the offset parameter, always appending to the end
152-
/// of the file instead.
152+
/// of the file instead. When available `pwritev2(..., RWF_NOAPPEND)` will
153+
/// be used instead to avoid this bug.
153154
///
154155
/// It is possible to inadvertently set this flag, like in the example below.
155156
/// Therefore, it is important to be vigilant while changing options to mitigate

library/std/src/os/unix/fs/tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ fn write_vectored_at() {
3939
file.write_all(msg).unwrap();
4040
}
4141
let expected = {
42-
let file = fs::File::options().write(true).open(&filename).unwrap();
42+
// Open in append mode to test that positioned writes bypass O_APPEND.
43+
let file = fs::File::options().append(true).open(&filename).unwrap();
4344
let buf0 = b" ";
4445
let buf1 = b"great ";
4546

library/std/src/sys/fd/unix.rs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ use crate::cmp;
2222
use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, Read};
2323
use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
2424
use crate::sys::cvt;
25-
#[cfg(all(target_os = "android", target_pointer_width = "64"))]
25+
#[cfg(all(any(target_os = "android", target_os = "linux"), target_pointer_width = "64"))]
2626
use crate::sys::pal::weak::syscall;
27-
#[cfg(any(all(target_os = "android", target_pointer_width = "32"), target_vendor = "apple"))]
27+
#[cfg(any(
28+
all(any(target_os = "android", target_os = "linux"), target_pointer_width = "32"),
29+
target_vendor = "apple"
30+
))]
2831
use crate::sys::pal::weak::weak;
2932
use crate::sys_common::{AsInner, FromInner, IntoInner};
3033

@@ -384,6 +387,16 @@ impl FileDesc {
384387
))]
385388
use libc::pwrite64;
386389

390+
// Work around linux deviating from POSIX where it ignores the
391+
// offset of pwrite when the file was opened with O_APPEND.
392+
#[cfg(any(target_os = "linux", target_os = "android"))]
393+
{
394+
let iov = [IoSlice::new(buf)];
395+
if let Some(ret) = self.pwritev2(&iov, offset) {
396+
return ret;
397+
}
398+
}
399+
387400
unsafe {
388401
cvt(pwrite64(
389402
self.as_raw_fd(),
@@ -408,6 +421,13 @@ impl FileDesc {
408421
target_os = "openbsd", // OpenBSD 2.7
409422
))]
410423
pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
424+
// Work around linux deviating from POSIX where it ignores the
425+
// offset of pwrite when the file was opened with O_APPEND.
426+
#[cfg(any(target_os = "linux", target_os = "android"))]
427+
if let Some(ret) = self.pwritev2(bufs, offset) {
428+
return ret;
429+
}
430+
411431
let ret = cvt(unsafe {
412432
libc::pwritev(
413433
self.as_raw_fd(),
@@ -611,6 +631,64 @@ impl FileDesc {
611631
pub fn duplicate(&self) -> io::Result<FileDesc> {
612632
Ok(Self(self.0.try_clone()?))
613633
}
634+
635+
#[cfg(any(target_os = "linux", target_os = "android"))]
636+
fn pwritev2(&self, bufs: &[IoSlice<'_>], offset: u64) -> Option<io::Result<usize>> {
637+
#[cfg(target_pointer_width = "64")]
638+
syscall!(
639+
fn pwritev2(
640+
fd: libc::c_int,
641+
iovec: *const libc::iovec,
642+
n_iovec: libc::c_int,
643+
offset: off64_t,
644+
flags: libc::c_int,
645+
) -> isize;
646+
);
647+
#[cfg(target_pointer_width = "32")]
648+
let pwritev2 = {
649+
weak!(
650+
fn pwritev2(
651+
fd: libc::c_int,
652+
iovec: *const libc::iovec,
653+
n_iovec: libc::c_int,
654+
offset: off64_t,
655+
flags: libc::c_int,
656+
) -> isize;
657+
);
658+
let Some(pwritev2) = pwritev2.get() else {
659+
return None;
660+
};
661+
pwritev2
662+
};
663+
664+
use core::sync::atomic::AtomicBool;
665+
666+
static NOAPPEND_SUPPORTED: AtomicBool = AtomicBool::new(true);
667+
if NOAPPEND_SUPPORTED.load(core::sync::atomic::Ordering::Relaxed) {
668+
let r = unsafe {
669+
cvt(pwritev2(
670+
self.as_raw_fd(),
671+
bufs.as_ptr() as *const libc::iovec,
672+
cmp::min(bufs.len(), max_iov()) as libc::c_int,
673+
offset as off64_t,
674+
libc::RWF_NOAPPEND,
675+
))
676+
};
677+
match r {
678+
Ok(ret) => return Some(Ok(ret as usize)),
679+
Err(e)
680+
if let Some(err) = e.raw_os_error()
681+
&& (err == libc::EOPNOTSUPP || err == libc::ENOSYS) =>
682+
{
683+
NOAPPEND_SUPPORTED.store(false, core::sync::atomic::Ordering::Relaxed);
684+
return None;
685+
}
686+
Err(e) => return Some(Err(e)),
687+
}
688+
}
689+
690+
return None;
691+
}
614692
}
615693

616694
impl<'a> Read for &'a FileDesc {

0 commit comments

Comments
 (0)