diff --git a/src/uu/chmod/locales/en-US.ftl b/src/uu/chmod/locales/en-US.ftl index 12df1e2b7a5..4294ab31fce 100644 --- a/src/uu/chmod/locales/en-US.ftl +++ b/src/uu/chmod/locales/en-US.ftl @@ -10,6 +10,7 @@ chmod-error-no-such-file = cannot access {$file}: No such file or directory chmod-error-preserve-root = it is dangerous to operate recursively on {$file} chmod: use --no-preserve-root to override this failsafe chmod-error-permission-denied = cannot access {$file}: Permission denied +chmod-error-read-only-file-system = changing permissions of {$file}: Read-only file system chmod-error-new-permissions = {$file}: new permissions are {$actual}, not {$expected} chmod-error-missing-operand = missing operand diff --git a/src/uu/chmod/locales/fr-FR.ftl b/src/uu/chmod/locales/fr-FR.ftl index f4e21b1b725..27ec0061356 100644 --- a/src/uu/chmod/locales/fr-FR.ftl +++ b/src/uu/chmod/locales/fr-FR.ftl @@ -22,6 +22,7 @@ chmod-error-no-such-file = impossible d'accéder à {$file} : Aucun fichier ou r chmod-error-preserve-root = il est dangereux d'opérer récursivement sur {$file} chmod: utiliser --no-preserve-root pour outrepasser cette protection chmod-error-permission-denied = impossible d'accéder à {$file} : Permission refusée +chmod-error-read-only-file-system = modification des permissions de {$file} : Système de fichiers en lecture seule chmod-error-new-permissions = {$file} : les nouvelles permissions sont {$actual}, pas {$expected} chmod-error-missing-operand = opérande manquant diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 6eb9add77ed..9274b2c6333 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -35,6 +35,8 @@ enum ChmodError { PreserveRoot(PathBuf), #[error("{}", translate!("chmod-error-permission-denied", "file" => _0.quote()))] PermissionDenied(PathBuf), + #[error("{}", translate!("chmod-error-read-only-file-system", "file" => _0.quote()))] + ReadOnlyFileSystem(PathBuf), #[error("{}", translate!("chmod-error-new-permissions", "file" => _0.maybe_quote(), "actual" => _1.clone(), "expected" => _2.clone()))] NewPermissions(PathBuf, String, String), } @@ -609,14 +611,20 @@ impl Chmoder { let (new_mode, _) = self.calculate_new_mode(current_mode, file_path.is_dir())?; // Use safe traversal to change the mode - if let Err(_e) = dir_fd.chmod_at(entry_name, new_mode, symlink_behavior) { + if let Err(e) = dir_fd.chmod_at(entry_name, new_mode, symlink_behavior) { if self.verbose { println!( "failed to change mode of {} to {new_mode:o}", file_path.quote(), ); } - return Err(ChmodError::PermissionDenied(file_path.into()).into()); + let err = if e.kind() == std::io::ErrorKind::ReadOnlyFilesystem { + ChmodError::ReadOnlyFileSystem(file_path.into()) + } else { + ChmodError::PermissionDenied(file_path.into()) + }; + + return Err(err.into()); } // Report the change using the helper method diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index a75324b7c67..bc9c95f3bf4 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -403,6 +403,60 @@ fn test_permission_denied() { .stderr_is("chmod: cannot access 'd/no-x/y': Permission denied\n"); } +#[test] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))] +fn test_chmod_readonly_filesystem() { + let mut scene = TestScenario::new(util_name!()); + + // Test must be run as root (or with `sudo -E`) + if scene.cmd("whoami").run().stdout_str() != "root\n" { + return; + } + + // Prepare the mount + let mountpoint = "readonly_mount"; + scene.fixtures.mkdir(mountpoint); + let mountpoint_path = scene.fixtures.plus_as_string(mountpoint); + + scene + .mount_temp_fs(&mountpoint_path) + .expect("mounting tmpfs failed"); + + // Create a file and set permissions so chmod will attempt to change them + scene.fixtures.touch(format!("{mountpoint}/file.txt")); + scene + .cmd("chmod") + .arg("400") + .arg(format!("{mountpoint_path}/file.txt")) + .run(); + + // Remount as read-only + scene + .cmd("mount") + .arg("-o") + .arg("remount,ro") + .arg(&mountpoint_path) + .run(); + + // Should say "Read-only file system" not "Permission denied" + scene + .ucmd() + .arg("ugo+w") + .arg(format!("{mountpoint_path}/file.txt")) + .fails() + .stderr_contains("Read-only file system"); + + // Remount as read-write so umount can clean up + scene + .cmd("mount") + .arg("-o") + .arg("remount,rw") + .arg(&mountpoint_path) + .run(); + + scene.umount_temp_fs(); +} + #[test] #[allow(clippy::unreadable_literal)] fn test_chmod_recursive_correct_exit_code() {