Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,12 @@ Recent decisions affecting current work:

### Pending Todos

6 pending todo(s):
5 pending todo(s):

- `2026-02-07-web-worker-large-file-encryption.md` -- Offload large file encryption to Web Worker (area: ui)
- `2026-02-14-bring-your-own-ipfs-node.md` -- Add bring-your-own IPFS node support (area: api)
- `2026-02-14-erc-1271-contract-wallet-authentication.md` -- Add ERC-1271 contract wallet authentication support (area: auth)
- `2026-02-15-security-review-medium-term-fixes.md` -- Security review medium-term fixes: H-08, M-07, M-11 (area: auth)
- `2026-02-18-fix-fuse-rename-on-smb-backend.md` -- Fix FUSE rename on SMB backend (area: desktop)
- `2026-02-20-desktop-auto-update.md` -- Add auto-update to desktop app via Tauri updater plugin (area: desktop)

### Roadmap Evolution
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ File mutations (create, write, delete, rename) trigger IPNS metadata publish via

### Known Limitations (macOS)

- **Rename (`mv`) fails with EPERM** — macOS SMB client rejects before reaching FUSE. Open issue.
- **Rename (`mv`)** — Fixed. The macOS SMB client calls `access(W_OK)` before rename; strict UID checking returned EACCES under SMB's proxied UID. Fix: `access()` now always grants (encryption is the access control). UID assignment in `create()`/`mkdir()` also unified to use `getuid()` for consistency.
- **Keychain prompts in debug builds** — each rebuild changes binary signature. Debug builds skip Keychain entirely (`#[cfg(debug_assertions)]`), using ephemeral UUIDs for device ID.
- **`opendir` must return non-zero file handles** — SMB treats `fh=0` as invalid.
- **No FSEvents on FUSE mounts** — Finder won't auto-refresh. CLI-created files appear in `ls` but not Finder until a new window is opened.
Expand Down
54 changes: 21 additions & 33 deletions apps/desktop/src-tauri/src/fuse/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ mod implementation {
/// The file isn't uploaded until release() -- it exists only locally.
fn create(
&mut self,
req: &Request<'_>,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
_mode: u32,
Expand Down Expand Up @@ -942,8 +942,11 @@ mod implementation {
// Allocate new inode
let ino = self.inodes.allocate_ino();
let now = SystemTime::now();
let uid = req.uid();
let gid = req.gid();
// Use process UID/GID (not req.uid/gid) for consistency with root
// inode and populate_folder. Under FUSE-T SMB, req.uid() may differ
// from the mounting user, causing permission mismatches.
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };

let attr = FileAttr {
ino,
Expand Down Expand Up @@ -1794,7 +1797,7 @@ mod implementation {
/// and updates the parent folder metadata.
fn mkdir(
&mut self,
req: &Request<'_>,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
_mode: u32,
Expand Down Expand Up @@ -1850,8 +1853,10 @@ mod implementation {
// Allocate inode and create InodeData (locally, no network I/O)
let ino = self.inodes.allocate_ino();
let now = SystemTime::now();
let uid = req.uid();
let gid = req.gid();
// Use process UID/GID for consistency with root inode and
// populate_folder (avoids SMB UID mismatch).
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };

let attr = FileAttr {
ino,
Expand Down Expand Up @@ -2336,43 +2341,26 @@ mod implementation {

/// Check file access permissions.
///
/// Enforces owner-only access based on inode permission bits.
/// Always grants access if the inode exists. CipherBox is a single-user
/// encrypted vault where client-side encryption is the access control
/// boundary — POSIX permission checks add no security value. Strict
/// UID/permission checking also breaks the FUSE-T SMB backend because
/// the SMB server proxies requests under a different UID than the
/// mounting user, causing `access(W_OK)` to fail and blocking rename.
fn access(
&mut self,
req: &Request<'_>,
_req: &Request<'_>,
ino: u64,
mask: i32,
reply: ReplyEmpty,
) {
let Some(inode) = self.inodes.get(ino) else {
if self.inodes.get(ino).is_none() {
reply.error(libc::ENOENT);
return;
};

// F_OK: just check existence
if mask == libc::F_OK {
reply.ok();
return;
}

let attr = &inode.attr;
// Owner-only check: only the mounting user should access files
if req.uid() != attr.uid {
reply.error(libc::EACCES);
return;
}

let owner_bits = (attr.perm >> 6) & 0o7;
let mut granted = true;
if mask & libc::R_OK != 0 && owner_bits & 0o4 == 0 { granted = false; }
if mask & libc::W_OK != 0 && owner_bits & 0o2 == 0 { granted = false; }
if mask & libc::X_OK != 0 && owner_bits & 0o1 == 0 { granted = false; }

if granted {
reply.ok();
} else {
reply.error(libc::EACCES);
}
log::trace!("access: ino={} mask={:#o} -> OK", ino, mask);
reply.ok();
}

/// Get extended attribute value.
Expand Down