passthrough.rs0.00%
1
// Copyright 2025 Google LLC2
//3
// Licensed under the Apache License, Version 2.0 (the "License");4
// you may not use this file except in compliance with the License.5
// You may obtain a copy of the License at6
//7
// https://www.apache.org/licenses/LICENSE-2.08
//9
// Unless required by applicable law or agreed to in writing, software10
// distributed under the License is distributed on an "AS IS" BASIS,11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12
// See the License for the specific language governing permissions and13
// limitations under the License.14
15
use std::collections::HashMap;16
use std::ffi::{CStr, OsStr};17
use std::fmt::Debug;18
use std::fs::{19
File, FileType, Metadata, OpenOptions, ReadDir, read_dir, remove_dir, remove_file, rename,20
};21
use std::io::{IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write};22
use std::iter::{Enumerate, Peekable};23
use std::marker::PhantomData;24
use std::os::fd::AsRawFd;25
use std::os::unix::ffi::OsStrExt;26
use std::os::unix::fs::{DirEntryExt, FileTypeExt, MetadataExt, OpenOptionsExt};27
use std::path::Path;28
29
use zerocopy::{FromBytes, IntoBytes};30
31
use crate::align_up_ty;32
use crate::fuse::bindings::{33
FUSE_KERNEL_MINOR_VERSION, FUSE_KERNEL_VERSION, FUSE_ROOT_ID, FuseAttr, FuseAttrOut,34
FuseCreateIn, FuseCreateOut, FuseDirent, FuseDirentType, FuseEntryOut, FuseFlushIn,35
FuseForgetIn, FuseGetattrFlag, FuseGetattrIn, FuseInHeader, FuseInitIn, FuseInitOut,36
FuseOpcode, FuseOpenIn, FuseOpenOut, FuseReadIn, FuseReleaseIn, FuseRemovemappingIn,37
FuseRemovemappingOne, FuseRename2In, FuseRenameIn, FuseSetupmappingFlag, FuseSetupmappingIn,38
FuseSyncfsIn, FuseWriteIn, FuseWriteOut, RenameFlag,39
};40
use crate::fuse::{DaxRegion, Fuse, Result, error};41
42
const MAX_BUFFER_SIZE: u32 = 1 << 20;43
44
fn fuse_dir_type(e: FileType) -> FuseDirentType {45
if e.is_dir() {46
FuseDirentType::DIR47
} else if e.is_file() {48
FuseDirentType::REG49
} else if e.is_symlink() {50
FuseDirentType::LNK51
} else if e.is_socket() {52
FuseDirentType::SOCK53
} else if e.is_file() {54
FuseDirentType::FIFO55
} else if e.is_char_device() {56
FuseDirentType::CHR57
} else if e.is_block_device() {58
FuseDirentType::BLK59
} else {60
FuseDirentType::UNKNOWN61
}62
}63
64
fn convert_o_flags(flags: i32) -> Result<OpenOptions> {65
let mut opts = OpenOptions::new();66
match flags & libc::O_ACCMODE {67
libc::O_RDONLY => opts.read(true),68
libc::O_WRONLY => opts.write(true),69
libc::O_RDWR => opts.read(true).write(true),70
mode => return error::InvalidAccMode { mode }.fail(),71
};72
opts.append(flags & libc::O_APPEND == libc::O_APPEND);73
opts.truncate(flags & libc::O_TRUNC == libc::O_TRUNC);74
opts.create(flags & libc::O_CREAT == libc::O_CREAT);75
opts.create_new(flags & (libc::O_CREAT | libc::O_EXCL) == libc::O_CREAT | libc::O_EXCL);76
let all = libc::O_ACCMODE | libc::O_APPEND | libc::O_TRUNC | libc::O_CREAT | libc::O_EXCL;77
opts.custom_flags(flags & !all);78
Ok(opts)79
}80
81
#[derive(Debug)]82
enum Handle {83
ReadDir(Box<Peekable<Enumerate<ReadDir>>>),84
File(File),85
}86
87
impl Handle {88
fn fh(&self) -> u64 {89
match self {90
Handle::File(f) => f.as_raw_fd() as u64,91
Handle::ReadDir(rd) => rd.as_ref() as *const Peekable<_> as u64,92
}93
}94
}95
96
#[derive(Debug)]97
struct Node {98
lookup_count: u64,99
path: Box<Path>,100
handle: Option<Handle>,101
}102
103
#[derive(Debug)]104
pub struct Passthrough {105
nodes: HashMap<u64, Node>,106
dax_region: Option<Box<dyn DaxRegion>>,107
}108
109
impl Passthrough {110
pub fn new(path: Box<Path>) -> Result<Self> {111
let node = Node {112
lookup_count: 1,113
path,114
handle: None,115
};116
let nodes = HashMap::from([(FUSE_ROOT_ID, node)]);117
Ok(Passthrough {118
nodes,119
dax_region: None,120
})121
}122
123
fn get_node(&self, id: u64) -> Result<&Node> {124
match self.nodes.get(&id) {125
Some(node) => Ok(node),126
None => error::NodeId { id }.fail(),127
}128
}129
130
fn get_node_mut(&mut self, id: u64) -> Result<&mut Node> {131
match self.nodes.get_mut(&id) {132
Some(node) => Ok(node),133
None => error::NodeId { id }.fail(),134
}135
}136
137
fn join_path(&self, parent: u64, name: &[u8]) -> Result<Box<Path>> {138
let parent = self.get_node(parent)?;139
let p = OsStr::from_bytes(CStr::from_bytes_until_nul(name)?.to_bytes());140
Ok(parent.path.join(p).into_boxed_path())141
}142
143
fn convert_meta(&self, meta: &Metadata) -> FuseAttr {144
FuseAttr {145
ino: meta.ino(),146
size: meta.size(),147
blocks: meta.blocks(),148
atime: meta.atime() as _,149
mtime: meta.mtime() as _,150
ctime: meta.ctime() as _,151
atimensec: meta.atime_nsec() as _,152
mtimensec: meta.mtime_nsec() as _,153
ctimensec: meta.ctime_nsec() as _,154
mode: meta.mode(),155
nlink: meta.nlink() as _,156
uid: meta.uid(),157
gid: meta.gid(),158
rdev: meta.rdev() as _,159
blksize: meta.blksize() as _,160
flags: 0,161
}162
}163
}164
165
impl Fuse for Passthrough {166
fn init(&mut self, _hdr: &FuseInHeader, in_: &FuseInitIn) -> Result<FuseInitOut> {167
Ok(FuseInitOut {168
major: FUSE_KERNEL_VERSION,169
minor: FUSE_KERNEL_MINOR_VERSION,170
max_readahead: in_.max_readahead,171
flags: 0,172
max_background: u16::MAX,173
congestion_threshold: (u16::MAX / 4) * 3,174
max_write: MAX_BUFFER_SIZE,175
time_gran: 1,176
max_pages: 256,177
map_alignment: 0,178
flags2: 0,179
..Default::default()180
})181
}182
183
fn set_dax_region(&mut self, dax_window: Box<dyn DaxRegion>) {184
self.dax_region = Some(dax_window)185
}186
187
fn get_attr(&mut self, hdr: &FuseInHeader, in_: &FuseGetattrIn) -> Result<FuseAttrOut> {188
let node = self.get_node(hdr.nodeid)?;189
log::trace!("get_attr: {in_:?} {:?}", node.path);190
191
let flag = FuseGetattrFlag::from_bits_retain(in_.getattr_flags);192
if flag.contains(FuseGetattrFlag::FH) {193
let Some(handle) = &node.handle else {194
return error::InvalidFileHandle.fail();195
};196
let fh = handle.fh();197
if in_.fh != fh {198
return error::InvalidFileHandle.fail();199
}200
}201
202
let file = OpenOptions::new()203
.read(true)204
.custom_flags(libc::O_NOFOLLOW)205
.open(&node.path)?;206
let meta = file.metadata()?;207
Ok(FuseAttrOut {208
attr_valid: 1,209
attr_valid_nsec: 0,210
attr: self.convert_meta(&meta),211
dummy: 0,212
})213
}214
215
fn open_dir(&mut self, hdr: &FuseInHeader, in_: &FuseOpenIn) -> Result<FuseOpenOut> {216
let node = self.get_node_mut(hdr.nodeid)?;217
log::trace!("open_dir: {in_:?} {:?}", node.path);218
let handle = Handle::ReadDir(Box::new(read_dir(&node.path)?.enumerate().peekable()));219
let fh = handle.fh();220
node.handle = Some(handle);221
Ok(FuseOpenOut {222
fh,223
open_flags: 0,224
backing_id: 0,225
})226
}227
228
fn read_dir(229
&mut self,230
hdr: &FuseInHeader,231
in_: &FuseReadIn,232
mut buf: &mut [u8],233
) -> Result<usize> {234
let node = self.get_node_mut(hdr.nodeid)?;235
log::trace!("read_dir: {:?}", node.path);236
237
let Some(Handle::ReadDir(read_dir)) = &mut node.handle else {238
return error::DirNotOpened.fail();239
};240
let Some((index, _)) = read_dir.peek() else {241
return Ok(0);242
};243
if *index as u64 != in_.offset {244
todo!("in_offset = {}, != {}", in_.offset, *index);245
}246
247
let mut total_len = 0;248
249
while let Some((index, entry)) = read_dir.peek() {250
let e = entry.as_ref()?;251
let name = e.file_name();252
let namelen = name.len();253
254
let dir_entry = FuseDirent {255
ino: e.ino(),256
off: *index as u64 + 1,257
namelen: namelen as _,258
type_: fuse_dir_type(e.file_type()?),259
name: PhantomData,260
};261
let aligned_namelen = align_up_ty!(namelen, FuseDirent);262
let len = size_of_val(&dir_entry) + aligned_namelen;263
let Some((p1, p2)) = buf.split_at_mut_checked(len) else {264
break;265
};266
let (b_entry, b_name) = p1.split_at_mut(size_of_val(&dir_entry));267
log::trace!("read_dir: {dir_entry:?} {name:?}");268
b_entry.copy_from_slice(dir_entry.as_bytes());269
b_name[..namelen].copy_from_slice(name.as_encoded_bytes());270
271
buf = p2;272
total_len += len;273
read_dir.next();274
}275
276
Ok(total_len)277
}278
279
fn release_dir(&mut self, hdr: &FuseInHeader, in_: &FuseReleaseIn) -> Result<()> {280
let node = self.get_node_mut(hdr.nodeid)?;281
node.handle = None;282
log::trace!("release_dir: {in_:?} {:?}", node.path);283
Ok(())284
}285
286
fn release(&mut self, hdr: &FuseInHeader, in_: &FuseReleaseIn) -> Result<()> {287
let node = self.get_node_mut(hdr.nodeid)?;288
node.handle = None;289
log::trace!("release: {in_:?} {:?}", node.path);290
Ok(())291
}292
293
fn lookup(&mut self, hdr: &FuseInHeader, in_: &[u8]) -> Result<FuseEntryOut> {294
let path = self.join_path(hdr.nodeid, in_)?;295
296
log::trace!("lookup: {path:?}");297
let file = OpenOptions::new()298
.read(true)299
.custom_flags(libc::O_NOFOLLOW)300
.open(&path)?;301
let meta = file.metadata()?;302
let nodeid =303
if let Some((nodeid, node)) = self.nodes.iter_mut().find(|(_, n)| n.path == path) {304
node.lookup_count += 1;305
*nodeid306
} else {307
let nodeid = path.as_os_str().as_bytes().as_ptr() as u64;308
let node = Node {309
lookup_count: 1,310
path,311
handle: None,312
};313
self.nodes.insert(nodeid, node);314
nodeid315
};316
Ok(FuseEntryOut {317
nodeid,318
generation: 0,319
entry_valid: 0,320
attr_valid: 0,321
entry_valid_nsec: 0,322
attr_valid_nsec: 0,323
attr: self.convert_meta(&meta),324
})325
}326
327
fn forget(&mut self, hdr: &FuseInHeader, in_: &FuseForgetIn) -> Result<()> {328
let node = self.get_node_mut(hdr.nodeid)?;329
log::trace!(330
"forget: {:?}, ref_count {}, remove {}",331
node.path,332
node.lookup_count,333
in_.nlookup334
);335
node.lookup_count -= in_.nlookup;336
if node.lookup_count == 0 {337
self.nodes.remove(&hdr.nodeid);338
}339
Ok(())340
}341
342
fn open(&mut self, hdr: &FuseInHeader, in_: &FuseOpenIn) -> Result<FuseOpenOut> {343
let node = self.get_node_mut(hdr.nodeid)?;344
log::trace!("open: {:?} {in_:?}", node.path);345
let opts = convert_o_flags(in_.flags as i32)?;346
let handle = Handle::File(opts.open(&node.path)?);347
let fh = handle.fh();348
node.handle = Some(handle);349
Ok(FuseOpenOut {350
fh,351
open_flags: 0,352
backing_id: 0,353
})354
}355
356
fn read(357
&mut self,358
hdr: &FuseInHeader,359
in_: &FuseReadIn,360
iov: &mut [IoSliceMut],361
) -> Result<usize> {362
let node = self.get_node(hdr.nodeid)?;363
log::trace!("read: {hdr:?} {in_:?} {:?}", node.path);364
let Some(Handle::File(f)) = &node.handle else {365
return error::FileNotOpened.fail();366
};367
let mut file = f;368
// TODO: use `read_vectored_at`369
// https://github.com/rust-lang/rust/issues/89517370
file.seek(SeekFrom::Start(in_.offset))?;371
let size = file.read_vectored(iov)?;372
Ok(size)373
}374
375
fn flush(&mut self, hdr: &FuseInHeader, in_: &FuseFlushIn) -> Result<()> {376
log::error!("flush: {hdr:?} {in_:?}");377
Ok(())378
}379
380
fn syncfs(&mut self, hdr: &FuseInHeader, in_: &FuseSyncfsIn) -> Result<()> {381
log::error!("syncfs: {hdr:?} {in_:?}");382
Ok(())383
}384
385
fn create(386
&mut self,387
hdr: &FuseInHeader,388
in_: &FuseCreateIn,389
buf: &[u8],390
) -> Result<FuseCreateOut> {391
let opts = convert_o_flags(in_.flags as i32)?;392
let path = self.join_path(hdr.nodeid, buf)?;393
let nodeid = path.as_os_str().as_bytes().as_ptr() as u64;394
let f = opts.open(&path)?;395
let meta = f.metadata()?;396
let handle = Handle::File(f);397
let fh = handle.fh();398
let node = Node {399
lookup_count: 1,400
path,401
handle: Some(handle),402
};403
self.nodes.insert(nodeid, node);404
Ok(FuseCreateOut {405
entry: FuseEntryOut {406
nodeid,407
generation: 0,408
entry_valid: 0,409
attr_valid: 0,410
entry_valid_nsec: 0,411
attr_valid_nsec: 0,412
attr: self.convert_meta(&meta),413
},414
open: FuseOpenOut {415
fh,416
open_flags: 0,417
backing_id: 0,418
},419
})420
}421
422
fn write(423
&mut self,424
hdr: &FuseInHeader,425
in_: &FuseWriteIn,426
buf: &[IoSlice],427
) -> Result<FuseWriteOut> {428
let node = self.get_node(hdr.nodeid)?;429
let Some(Handle::File(f)) = &node.handle else {430
return error::FileNotOpened.fail();431
};432
let mut file = f;433
// TODO: use `write_vectored_at`434
// https://github.com/rust-lang/rust/issues/89517435
file.seek(SeekFrom::Start(in_.offset))?;436
let size = file.write_vectored(buf)? as u32;437
Ok(FuseWriteOut { size, padding: 0 })438
}439
440
fn unlink(&mut self, hdr: &FuseInHeader, in_: &[u8]) -> Result<()> {441
let path = self.join_path(hdr.nodeid, in_)?;442
remove_file(&path)?;443
log::trace!("unlink: {path:?}");444
Ok(())445
}446
447
fn rmdir(&mut self, hdr: &FuseInHeader, in_: &[u8]) -> Result<()> {448
let path = self.join_path(hdr.nodeid, in_)?;449
remove_dir(path)?;450
Ok(())451
}452
453
fn rename(&mut self, hdr: &FuseInHeader, in_: &FuseRenameIn, buf: &[u8]) -> Result<()> {454
let in2 = FuseRename2In {455
newdir: in_.newdir,456
flags: 0,457
padding: 0,458
};459
self.rename2(hdr, &in2, buf)460
}461
462
fn rename2(&mut self, hdr: &FuseInHeader, in_: &FuseRename2In, buf: &[u8]) -> Result<()> {463
// TODO: use split_once464
// https://github.com/rust-lang/rust/issues/112811465
let mut paths = buf.split_inclusive(|b| *b == b'\0');466
let Some(p1) = paths.next() else {467
return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;468
};469
let Some(p2) = paths.next() else {470
return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;471
};472
let flag = RenameFlag::from_bits_retain(in_.flags);473
if !flag.is_empty() {474
return error::Unsupported {475
op: FuseOpcode::RENAME2,476
flag: flag.bits(),477
}478
.fail();479
}480
let src = self.join_path(hdr.nodeid, p1)?;481
let dst = self.join_path(in_.newdir, p2)?;482
rename(src, dst)?;483
Ok(())484
}485
486
fn setup_mapping(&mut self, hdr: &FuseInHeader, in_: &FuseSetupmappingIn) -> Result<()> {487
let Some(dax_region) = &self.dax_region else {488
return Err(std::io::Error::from_raw_os_error(libc::ENOSYS))?;489
};490
491
let node = self.get_node(hdr.nodeid)?;492
let Some(Handle::File(fd)) = &node.handle else {493
return error::FileNotOpened.fail();494
};495
496
let flag = FuseSetupmappingFlag::from_bits_retain(in_.flags);497
dax_region.map(in_.moffset, fd, in_.foffset, in_.len, flag)?;498
499
log::trace!(500
"setup_mapping: offset = {:#x}, file = {:?}",501
in_.moffset,502
node.path503
);504
505
Ok(())506
}507
508
fn remove_mapping(&mut self, _hdr: &FuseInHeader, in_: &[u8]) -> Result<()> {509
let Some(dax_region) = &self.dax_region else {510
return Err(std::io::Error::from_raw_os_error(libc::ENOSYS))?;511
};512
513
let Ok((h, mut remain)) = FuseRemovemappingIn::ref_from_prefix(in_) else {514
return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;515
};516
517
for _ in 0..h.count {518
let Ok((one, r)) = FuseRemovemappingOne::read_from_prefix(remain) else {519
return Err(std::io::Error::from_raw_os_error(libc::EINVAL))?;520
};521
dax_region.unmap(one.moffset, one.len)?;522
log::trace!(523
"remove_mapping: offset = {:#x}, size = {:#x}",524
one.moffset,525
one.len526
);527
528
remain = r;529
}530
531
Ok(())532
}533
}534