Skip to content

Commit 431d6b1

Browse files
fix(vfs): harden completion and path boundaries
1 parent a6c3e18 commit 431d6b1

17 files changed

Lines changed: 1658 additions & 191 deletions

src/vfs/gcd/file.rs

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! `GcdFile`: per-file I/O against a `DispatchIO` channel.
22
//!
33
//! The channel is created in `DISPATCH_IO_RANDOM` mode and owns a duplicated
4-
//! file descriptor (so dispatch_io can asynchronously close its own copy when
4+
//! file descriptor (so `dispatch_io` can asynchronously close its own copy when
55
//! the channel is released, independently of the `std::fs::File` we hold).
66
//! Reads and writes call `dispatch_io_read` / `dispatch_io_write` with block
77
//! handlers; the handlers accumulate chunks and, on `done`, send the result
@@ -14,12 +14,12 @@ use std::sync::Arc;
1414

1515
use parking_lot::Mutex;
1616

17-
use block2::{Block, DynBlock, RcBlock};
17+
use block2::{DynBlock, RcBlock};
1818
use dispatch2::{DispatchData, DispatchIO, DispatchIOCloseFlags, DispatchQueue, DispatchRetained};
1919

2020
use crate::Result;
2121
use crate::errors::PagedbError;
22-
use crate::vfs::traits::VfsFile;
22+
use crate::vfs::traits::{VfsFile, checked_signed_file_len};
2323
use crate::vfs::types::{ReadReq, WriteReq};
2424

2525
pub struct GcdFile {
@@ -81,6 +81,37 @@ impl GcdFile {
8181
fn fd(&self) -> std::os::unix::io::RawFd {
8282
self.file.as_raw_fd()
8383
}
84+
85+
fn checked_dispatch_offset(offset: u64, len: usize) -> Result<libc::off_t> {
86+
let last = if len > 0 {
87+
let last_delta = u64::try_from(len - 1).map_err(|_| {
88+
PagedbError::Io(std::io::Error::new(
89+
std::io::ErrorKind::InvalidInput,
90+
"buffer length does not fit in u64",
91+
))
92+
})?;
93+
offset.checked_add(last_delta).ok_or_else(|| {
94+
PagedbError::Io(std::io::Error::new(
95+
std::io::ErrorKind::InvalidInput,
96+
"dispatch_io offset range overflow",
97+
))
98+
})?
99+
} else {
100+
offset
101+
};
102+
libc::off_t::try_from(last).map_err(|_| {
103+
PagedbError::Io(std::io::Error::new(
104+
std::io::ErrorKind::InvalidInput,
105+
"dispatch_io offset range does not fit into libc::off_t",
106+
))
107+
})?;
108+
offset.try_into().map_err(|_| {
109+
PagedbError::Io(std::io::Error::new(
110+
std::io::ErrorKind::InvalidInput,
111+
"dispatch_io offset does not fit into libc::off_t",
112+
))
113+
})
114+
}
84115
}
85116

86117
impl Drop for GcdFile {
@@ -100,7 +131,7 @@ impl Drop for GcdFile {
100131
fn submit_read(
101132
channel: &DispatchIO,
102133
queue: &DispatchQueue,
103-
offset: u64,
134+
offset: libc::off_t,
104135
len: usize,
105136
tx: tokio::sync::oneshot::Sender<std::io::Result<Vec<u8>>>,
106137
) {
@@ -135,22 +166,15 @@ fn submit_read(
135166
}
136167
});
137168

138-
// SAFETY: transmute is from the stand-in block signature to the typedef
139-
// declared by dispatch2 — the ABI is identical because `bool` and `u8`
140-
// share the same one-byte ABI, and a `*mut DispatchData` is bit-identical
141-
// to a `*mut c_void`.
142-
let handler_ptr: *mut DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)> = unsafe {
143-
std::mem::transmute::<
144-
*mut Block<dyn Fn(u8, *mut c_void, libc::c_int)>,
145-
*mut DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)>,
146-
>(RcBlock::as_ptr(&handler))
147-
};
169+
// SAFETY: ABI-compatible pointer cast to the typedef declared by dispatch2.
170+
// `bool` and `u8` are one byte, while both data arguments are pointers.
171+
let handler_ptr: *mut DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)> =
172+
RcBlock::as_ptr(&handler).cast::<DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)>>();
148173

149174
// SAFETY: channel and queue are valid (owned by the caller's `GcdFile`);
150175
// the handler block is retained by libdispatch on submission.
151176
unsafe {
152-
#[allow(clippy::cast_possible_wrap)]
153-
channel.read(offset as libc::off_t, len, queue, handler_ptr);
177+
channel.read(offset, len, queue, handler_ptr);
154178
}
155179
// libdispatch has retained the block internally; we can drop our Rc.
156180
drop(handler);
@@ -162,7 +186,7 @@ fn submit_read(
162186
fn submit_write(
163187
channel: &DispatchIO,
164188
queue: &DispatchQueue,
165-
offset: u64,
189+
offset: libc::off_t,
166190
buf: &[u8],
167191
tx: tokio::sync::oneshot::Sender<std::io::Result<()>>,
168192
) {
@@ -185,19 +209,14 @@ fn submit_write(
185209
},
186210
);
187211

188-
// SAFETY: ABI-compatible transmute; see `submit_read`.
189-
let handler_ptr: *mut DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)> = unsafe {
190-
std::mem::transmute::<
191-
*mut Block<dyn Fn(u8, *mut c_void, libc::c_int)>,
192-
*mut DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)>,
193-
>(RcBlock::as_ptr(&handler))
194-
};
212+
// SAFETY: ABI-compatible pointer cast; see `submit_read`.
213+
let handler_ptr: *mut DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)> =
214+
RcBlock::as_ptr(&handler).cast::<DynBlock<dyn Fn(bool, *mut DispatchData, libc::c_int)>>();
195215

196216
// SAFETY: channel/queue/data all valid; libdispatch retains both the data
197217
// object and the handler block for the operation's duration.
198218
unsafe {
199-
#[allow(clippy::cast_possible_wrap)]
200-
channel.write(offset as libc::off_t, &data, queue, handler_ptr);
219+
channel.write(offset, &data, queue, handler_ptr);
201220
}
202221
drop(handler);
203222
drop(data);
@@ -209,6 +228,7 @@ impl VfsFile for GcdFile {
209228
return Ok(0);
210229
}
211230
let len = buf.len();
231+
let offset = Self::checked_dispatch_offset(offset, len)?;
212232
let (tx, rx) = tokio::sync::oneshot::channel::<std::io::Result<Vec<u8>>>();
213233
// The handler block and its raw block pointer are `!Send`; submitting
214234
// from a synchronous helper keeps them out of this future's state
@@ -227,6 +247,10 @@ impl VfsFile for GcdFile {
227247
}
228248

229249
async fn read_at_vectored(&self, reqs: &mut [ReadReq<'_>]) -> Result<()> {
250+
for req in reqs.iter() {
251+
Self::checked_dispatch_offset(req.offset, req.buf.len())?;
252+
}
253+
230254
// dispatch_io operations are intrinsically sequential per channel
231255
// (the channel serialises ops in submission order); issuing them one
232256
// at a time matches that and keeps the bridge simple.
@@ -247,6 +271,7 @@ impl VfsFile for GcdFile {
247271
return Ok(0);
248272
}
249273
let len = buf.len();
274+
let offset = Self::checked_dispatch_offset(offset, len)?;
250275
let (tx, rx) = tokio::sync::oneshot::channel::<std::io::Result<()>>();
251276
// `!Send` handler/data confined to a synchronous helper; see `read_at`.
252277
submit_write(&self.channel, &self.queue, offset, buf, tx);
@@ -261,6 +286,9 @@ impl VfsFile for GcdFile {
261286
if !self.writable {
262287
return Err(PagedbError::ReadOnly);
263288
}
289+
for req in reqs {
290+
Self::checked_dispatch_offset(req.offset, req.buf.len())?;
291+
}
264292
for req in reqs {
265293
self.write_at(req.offset, req.buf).await?;
266294
}
@@ -282,10 +310,10 @@ impl VfsFile for GcdFile {
282310
if !self.writable {
283311
return Err(PagedbError::ReadOnly);
284312
}
285-
// SAFETY: `fd()` valid as above; `len` fits in `libc::off_t` on
286-
// 64-bit Apple targets (`off_t` is `i64` on macOS / iOS).
287-
#[allow(clippy::cast_possible_wrap)]
288-
let rc = unsafe { libc::ftruncate(self.fd(), len as libc::off_t) };
313+
let len = checked_signed_file_len(len, "ftruncate")?;
314+
// SAFETY: `fd()` is valid as above and `len` was checked to fit the
315+
// signed native file-offset type.
316+
let rc = unsafe { libc::ftruncate(self.fd(), len) };
289317
if rc != 0 {
290318
return Err(PagedbError::Io(std::io::Error::last_os_error()));
291319
}

src/vfs/gcd/vfs.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::Result;
1717
use crate::errors::PagedbError;
1818

1919
use super::file::GcdFile;
20-
use crate::vfs::traits::Vfs;
20+
use crate::vfs::traits::{Vfs, canonical_native_path, resolve_native_path};
2121
use crate::vfs::types::OpenMode;
2222

2323
#[derive(Debug, Clone, Copy)]
@@ -128,8 +128,8 @@ impl GcdVfs {
128128
}
129129
}
130130

131-
fn resolve(&self, p: &str) -> PathBuf {
132-
self.inner.root.join(p.trim_start_matches('/'))
131+
fn resolve(&self, path: &str) -> Result<PathBuf> {
132+
resolve_native_path(&self.inner.root, path)
133133
}
134134

135135
fn lookup_or_create_entry(&self, path: &str) -> Arc<InProcLockEntry> {
@@ -145,7 +145,8 @@ impl GcdVfs {
145145
}
146146

147147
fn do_lock(&self, path: &str, kind: LockKind) -> Result<GcdLockHandle> {
148-
let entry = self.lookup_or_create_entry(path);
148+
let logical_path = canonical_native_path(path)?;
149+
let entry = self.lookup_or_create_entry(&logical_path);
149150
{
150151
let mut s = entry.state.lock();
151152
match (kind, *s) {
@@ -155,7 +156,7 @@ impl GcdVfs {
155156
_ => return Err(PagedbError::AlreadyLocked),
156157
}
157158
}
158-
let lock_path = self.resolve(path);
159+
let lock_path = self.resolve(&logical_path)?;
159160
if let Some(parent) = lock_path.parent() {
160161
std::fs::create_dir_all(parent).map_err(PagedbError::Io)?;
161162
}
@@ -184,7 +185,7 @@ impl Vfs for GcdVfs {
184185
type LockHandle = GcdLockHandle;
185186

186187
async fn open(&self, path: &str, mode: OpenMode) -> Result<Self::File> {
187-
let p = self.resolve(path);
188+
let p = self.resolve(path)?;
188189
if matches!(mode, OpenMode::CreateNew | OpenMode::CreateOrOpen) {
189190
if let Some(parent) = p.parent() {
190191
std::fs::create_dir_all(parent).map_err(PagedbError::Io)?;
@@ -231,7 +232,7 @@ impl Vfs for GcdVfs {
231232
}
232233

233234
async fn remove(&self, path: &str) -> Result<()> {
234-
let p = self.resolve(path);
235+
let p = self.resolve(path)?;
235236
match std::fs::remove_file(&p) {
236237
Ok(()) => Ok(()),
237238
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
@@ -240,16 +241,16 @@ impl Vfs for GcdVfs {
240241
}
241242

242243
async fn rename(&self, from: &str, to: &str) -> Result<()> {
243-
let f = self.resolve(from);
244-
let t = self.resolve(to);
244+
let f = self.resolve(from)?;
245+
let t = self.resolve(to)?;
245246
if let Some(parent) = t.parent() {
246247
std::fs::create_dir_all(parent).map_err(PagedbError::Io)?;
247248
}
248249
std::fs::rename(&f, &t).map_err(PagedbError::Io)
249250
}
250251

251252
async fn list_dir(&self, path: &str) -> Result<Vec<String>> {
252-
let p = self.resolve(path);
253+
let p = self.resolve(path)?;
253254
let iter = match std::fs::read_dir(&p) {
254255
Ok(it) => it,
255256
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
@@ -267,13 +268,13 @@ impl Vfs for GcdVfs {
267268
}
268269

269270
async fn mkdir_all(&self, path: &str) -> Result<()> {
270-
let p = self.resolve(path);
271+
let p = self.resolve(path)?;
271272
std::fs::create_dir_all(&p).map_err(PagedbError::Io)
272273
}
273274

274275
async fn sync_dir(&self, path: &str) -> Result<()> {
275276
// POSIX fsync on the directory fd; HFS+/APFS honor it.
276-
let p = self.resolve(path);
277+
let p = self.resolve(path)?;
277278
let dir = match std::fs::File::open(&p) {
278279
Ok(d) => d,
279280
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),

0 commit comments

Comments
 (0)