Skip to content

Commit 00af197

Browse files
committed
fix handling of symlinks in recursive rename
1 parent 6253cdb commit 00af197

2 files changed

Lines changed: 285 additions & 3 deletions

File tree

src/fs.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,11 +310,62 @@ impl EncFs {
310310
}
311311
} else if self.external_iv_chaining && meta.is_file() {
312312
self.copy_file_with_header_rewrite(source.physical, dest.physical, source.iv, dest.iv)?;
313-
} else {
314-
// Standard copy
315-
if meta.is_symlink() {
313+
} else if meta.is_symlink() {
314+
// Handle symlinks during recursive directory copies.
315+
// When chained_name_iv is enabled, symlink targets are encrypted using
316+
// the path IV of the symlink. If the symlink's path changes (due to parent
317+
// directory rename), we need to re-encrypt the target with the new IV.
318+
if self.external_iv_chaining {
319+
// External IV chaining for symlinks is not supported
316320
return Err(libc::ENOSYS);
317321
}
322+
323+
if self.chained_name_iv {
324+
// Re-encrypt symlink target with the new path IV
325+
let target = fs::read_link(source.physical)
326+
.map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
327+
let target_str = target.to_str().ok_or(libc::EILSEQ)?;
328+
329+
let (plain_target, _) = self
330+
.cipher
331+
.decrypt_filename(target_str, source.iv)
332+
.map_err(|e| {
333+
error!(
334+
"Failed to decrypt symlink target during recursive copy: {}",
335+
e
336+
);
337+
libc::EIO
338+
})?;
339+
340+
let (enc_target, _) = self
341+
.cipher
342+
.encrypt_filename(&plain_target, dest.iv)
343+
.map_err(|e| {
344+
error!(
345+
"Failed to encrypt symlink target during recursive copy: {}",
346+
e
347+
);
348+
libc::EIO
349+
})?;
350+
351+
// Remove existing destination if present
352+
match fs::remove_file(dest.physical) {
353+
Ok(_) => {}
354+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
355+
Err(e) => return Err(e.raw_os_error().unwrap_or(libc::EIO)),
356+
}
357+
358+
std::os::unix::fs::symlink(Path::new(&enc_target), dest.physical)
359+
.map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
360+
} else {
361+
// No IV chaining - just copy the symlink as-is
362+
let target = fs::read_link(source.physical)
363+
.map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
364+
std::os::unix::fs::symlink(&target, dest.physical)
365+
.map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
366+
}
367+
} else {
368+
// Standard copy for regular files without external IV chaining
318369
fs::copy(source.physical, dest.physical)
319370
.map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
320371
// Best effort metadata copy
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/// Test for: Rename fails for directories containing symlinks with IV chaining
2+
///
3+
/// When `chained_name_iv` is enabled and a directory containing symlinks is renamed,
4+
/// the `copy_recursive` function fails with ENOSYS because it doesn't handle symlinks -
5+
/// it only handles files and directories. The standalone symlink rename (lines 194-238
6+
/// in fs.rs) correctly re-encrypts the symlink target with the new path IV, but this
7+
/// logic isn't applied during recursive directory copies.
8+
use encfs::config::Interface;
9+
use encfs::crypto::ssl::SslCipher;
10+
use encfs::fs::EncFs;
11+
use fuse_mt::{FilesystemMT, RequestInfo};
12+
use std::ffi::OsStr;
13+
use std::fs;
14+
use std::path::{Path, PathBuf};
15+
16+
fn setup_fs(root: &Path) -> EncFs {
17+
let iface = Interface {
18+
name: "ssl/aes".to_string(),
19+
major: 3,
20+
minor: 0,
21+
age: 0,
22+
};
23+
let cipher = SslCipher::new(&iface, 192).unwrap();
24+
let mut cipher = cipher;
25+
let user_key = vec![1u8; 24];
26+
let user_iv = vec![2u8; 16];
27+
cipher.set_key(&user_key, &user_iv);
28+
29+
// chained_name_iv=true triggers the bug
30+
EncFs::new(root.to_path_buf(), cipher, 1024, 8, true, false)
31+
}
32+
33+
fn req() -> RequestInfo {
34+
RequestInfo {
35+
unique: 1,
36+
pid: 1,
37+
gid: 0,
38+
uid: 0,
39+
}
40+
}
41+
42+
#[test]
43+
fn test_rename_directory_containing_symlink_with_chained_name_iv() {
44+
let _ = env_logger::builder().is_test(true).try_init();
45+
let tmp = std::env::temp_dir().join("encfs_rename_symlink_dir_test");
46+
if tmp.exists() {
47+
fs::remove_dir_all(&tmp).unwrap();
48+
}
49+
fs::create_dir(&tmp).unwrap();
50+
51+
let fs = setup_fs(&tmp);
52+
let r = req();
53+
54+
// Create a directory "parent"
55+
let parent_path = PathBuf::from("/parent");
56+
fs.mkdir(r, Path::new("/"), OsStr::new("parent"), 0o755)
57+
.expect("mkdir parent failed");
58+
59+
// Create a symlink inside the directory
60+
let target = Path::new("some_target");
61+
fs.symlink(r, &parent_path, OsStr::new("link"), target)
62+
.expect("symlink inside dir failed");
63+
64+
// Create a regular file inside the directory for comparison
65+
let created = fs
66+
.create(
67+
r,
68+
&parent_path,
69+
OsStr::new("file.txt"),
70+
0o644,
71+
(libc::O_CREAT | libc::O_RDWR) as u32,
72+
)
73+
.expect("create file failed");
74+
let _ = fs.release(
75+
r,
76+
&PathBuf::from("/parent/file.txt"),
77+
created.fh,
78+
0,
79+
0,
80+
true,
81+
);
82+
83+
// Verify the symlink can be read before rename
84+
let readlink_result = fs.readlink(r, &PathBuf::from("/parent/link"));
85+
assert!(
86+
readlink_result.is_ok(),
87+
"readlink before rename failed: {:?}",
88+
readlink_result.err()
89+
);
90+
let target_bytes = readlink_result.unwrap();
91+
assert_eq!(
92+
String::from_utf8_lossy(&target_bytes),
93+
"some_target",
94+
"symlink target mismatch before rename"
95+
);
96+
97+
// Rename the directory containing the symlink
98+
// This is the operation that triggers the bug (ENOSYS due to symlink handling)
99+
let rename_result = fs.rename(
100+
r,
101+
Path::new("/"),
102+
OsStr::new("parent"),
103+
Path::new("/"),
104+
OsStr::new("renamed_parent"),
105+
);
106+
107+
assert!(
108+
rename_result.is_ok(),
109+
"rename directory with symlink failed: error code {:?}",
110+
rename_result.err()
111+
);
112+
113+
// Verify the symlink target can still be read after rename
114+
let readlink_after = fs.readlink(r, &PathBuf::from("/renamed_parent/link"));
115+
assert!(
116+
readlink_after.is_ok(),
117+
"readlink after rename failed: {:?}",
118+
readlink_after.err()
119+
);
120+
let target_after = readlink_after.unwrap();
121+
assert_eq!(
122+
String::from_utf8_lossy(&target_after),
123+
"some_target",
124+
"symlink target should be 'some_target' after rename, but was '{}'",
125+
String::from_utf8_lossy(&target_after)
126+
);
127+
128+
// Verify the old path no longer exists
129+
let old_path_result = fs.getattr(r, &PathBuf::from("/parent"), None);
130+
assert!(
131+
old_path_result.is_err(),
132+
"old path should not exist after rename"
133+
);
134+
135+
// Verify the regular file also works after rename
136+
let file_attr = fs.getattr(r, &PathBuf::from("/renamed_parent/file.txt"), None);
137+
assert!(
138+
file_attr.is_ok(),
139+
"regular file should exist after rename: {:?}",
140+
file_attr.err()
141+
);
142+
143+
// Cleanup
144+
fs::remove_dir_all(&tmp).unwrap();
145+
}
146+
147+
#[test]
148+
fn test_rename_nested_directory_with_symlinks_chained_name_iv() {
149+
let _ = env_logger::builder().is_test(true).try_init();
150+
let tmp = std::env::temp_dir().join("encfs_rename_nested_symlink_test");
151+
if tmp.exists() {
152+
fs::remove_dir_all(&tmp).unwrap();
153+
}
154+
fs::create_dir(&tmp).unwrap();
155+
156+
let fs = setup_fs(&tmp);
157+
let r = req();
158+
159+
// Create nested directories: /outer/inner
160+
fs.mkdir(r, Path::new("/"), OsStr::new("outer"), 0o755)
161+
.expect("mkdir outer failed");
162+
fs.mkdir(r, Path::new("/outer"), OsStr::new("inner"), 0o755)
163+
.expect("mkdir inner failed");
164+
165+
// Create symlinks at different levels
166+
fs.symlink(
167+
r,
168+
&PathBuf::from("/outer"),
169+
OsStr::new("link1"),
170+
Path::new("../some_target"),
171+
)
172+
.expect("symlink in outer failed");
173+
fs.symlink(
174+
r,
175+
&PathBuf::from("/outer/inner"),
176+
OsStr::new("link2"),
177+
Path::new("../../other_target"),
178+
)
179+
.expect("symlink in inner failed");
180+
181+
// Verify symlinks before rename
182+
let link1_before = fs
183+
.readlink(r, &PathBuf::from("/outer/link1"))
184+
.expect("readlink link1 before failed");
185+
let link2_before = fs
186+
.readlink(r, &PathBuf::from("/outer/inner/link2"))
187+
.expect("readlink link2 before failed");
188+
assert_eq!(String::from_utf8_lossy(&link1_before), "../some_target");
189+
assert_eq!(String::from_utf8_lossy(&link2_before), "../../other_target");
190+
191+
// Rename the outer directory
192+
let rename_result = fs.rename(
193+
r,
194+
Path::new("/"),
195+
OsStr::new("outer"),
196+
Path::new("/"),
197+
OsStr::new("moved"),
198+
);
199+
200+
assert!(
201+
rename_result.is_ok(),
202+
"rename nested directory with symlinks failed: error code {:?}",
203+
rename_result.err()
204+
);
205+
206+
// Verify symlinks after rename
207+
let link1_after = fs.readlink(r, &PathBuf::from("/moved/link1"));
208+
assert!(
209+
link1_after.is_ok(),
210+
"readlink link1 after failed: {:?}",
211+
link1_after.err()
212+
);
213+
assert_eq!(
214+
String::from_utf8_lossy(&link1_after.unwrap()),
215+
"../some_target"
216+
);
217+
218+
let link2_after = fs.readlink(r, &PathBuf::from("/moved/inner/link2"));
219+
assert!(
220+
link2_after.is_ok(),
221+
"readlink link2 after failed: {:?}",
222+
link2_after.err()
223+
);
224+
assert_eq!(
225+
String::from_utf8_lossy(&link2_after.unwrap()),
226+
"../../other_target"
227+
);
228+
229+
// Cleanup
230+
fs::remove_dir_all(&tmp).unwrap();
231+
}

0 commit comments

Comments
 (0)