|
| 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