Skip to content
Open
2 changes: 1 addition & 1 deletion src/uu/install/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ install-help-unprivileged = do not require elevated privileges to change the own

# Error messages
install-error-dir-needs-arg = { $util_name } with -d requires at least one argument.
install-error-create-dir-failed = cannot create directory { $path }
install-error-create-dir-failed = cannot create directory { $path }: { $error }
install-error-chmod-failed = failed to chmod { $path }
install-error-chmod-failed-detailed = { $path }: chmod failed with error { $error }
install-error-chown-failed = failed to chown { $path }: { $error }
Expand Down
2 changes: 1 addition & 1 deletion src/uu/install/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ install-help-unprivileged = ne pas nécessiter de privilèges élevés pour chan

# Messages d'erreur
install-error-dir-needs-arg = { $util_name } avec -d nécessite au moins un argument.
install-error-create-dir-failed = échec de la création de { $path }
install-error-create-dir-failed = échec de la création de { $path } : { $error }
install-error-chmod-failed = échec du chmod { $path }
install-error-chmod-failed-detailed = { $path } : échec du chmod avec l'erreur { $error }
install-error-chown-failed = échec du chown { $path } : { $error }
Expand Down
51 changes: 25 additions & 26 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore (ToDO) rwxr sourcepath targetpath Isnt uioerror matchpathcon
// spell-checker:ignore (ToDO) rwxr sourcepath targetpath Isnt uioerror matchpathcon ENOTDIR

mod mode;

Expand All @@ -28,7 +28,9 @@ use uucore::error::{FromIo, UError, UResult, UUsageError, strip_errno};
use uucore::fs::{are_files_identical, dir_strip_dot_for_creation};
use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown};
#[cfg(unix)]
use uucore::safe_traversal::{DirFd, SymlinkBehavior, create_dir_all_safe};
use uucore::safe_traversal::{
DirFd, SymlinkBehavior, create_dir_all_safe, failed_create_dir_prefix,
};
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
use uucore::selinux::{
SeLinuxError, contexts_differ, get_selinux_security_context, is_selinux_enabled,
Expand Down Expand Up @@ -72,7 +74,7 @@ enum InstallError {
#[error("{}", translate!("install-error-dir-needs-arg", "util_name" => "install"))]
DirNeedsArg,

#[error("{}", translate!("install-error-create-dir-failed", "path" => .0.quote()))]
#[error("{}", translate!("install-error-create-dir-failed", "path" => .0.quote(), "error" => strip_errno(.1)))]
CreateDirFailed(PathBuf, #[source] std::io::Error),

#[error("{}", translate!("install-error-chmod-failed", "path" => .0.quote()))]
Expand Down Expand Up @@ -508,10 +510,17 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
// target directory. All created ancestor directories will have
// the default mode. Hence it is safe to use fs::create_dir_all
// and then only modify the target's dir mode.
if let Err(e) = fs::create_dir_all(&path_to_create).map_err_context(
|| translate!("install-error-create-dir-failed", "path" => path_to_create.quote()),
) {
show!(e);
//
// This stays path-based on purpose. Anchoring the walk to
// directory fds needs read permission on each existing ancestor,
// while mkdir only needs write and execute, so an fd walk fails on
// write-only directories where GNU succeeds.
if let Err(e) = fs::create_dir_all(&path_to_create) {
#[cfg(unix)]
let failed = failed_create_dir_prefix(&path_to_create);
#[cfg(not(unix))]
let failed = path_to_create.clone();
show!(InstallError::CreateDirFailed(failed, e));
continue;
}

Expand Down Expand Up @@ -658,11 +667,6 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
None
};

// If -t is used, check if target exists as a file before trying to create directories
if b.target_dir.is_some() && target.exists() && !target.is_dir() {
return Err(InstallError::NotADirectory(target).into());
}

if let Some(to_create) = to_create {
let to_create_original = to_create;
let to_create_owned;
Expand All @@ -679,6 +683,14 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
_ => to_create,
};

// With -t, GNU reports a target that exists but is not a directory
// as a failure to access it, rather than a failed creation. The
// check runs on the trimmed path so that a trailing slash, which
// makes `exists()` fail with ENOTDIR, is handled the same way.
if b.target_dir.is_some() && to_create.exists() && !to_create.is_dir() {
return Err(InstallError::NotADirectory(to_create_original.to_path_buf()).into());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that cause a TOCTOU?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only decides which error gets printed. If the target is an existing non-directory, install stops with "Not a directory"; otherwise nothing is done with the result. If the path changes after the check, the directories are still created through create_dir_all_safe, which works through directory descriptors (mkdirat, openat with O_DIRECTORY) and refuses a file or dangling symlink at that name by itself. So a race can only change which message you get, to "cannot create directory". The check isn't new either: main already does the same exists()/is_dir() check on the -t target; this moves it after the trailing-slash trim.

}
Comment thread
Copilot marked this conversation as resolved.

let dir_exists = to_create.exists() && metadata(to_create).is_ok_and(|m| m.is_dir());

if dir_exists {
Expand Down Expand Up @@ -735,20 +747,7 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::AlreadyExists
&& to_create.exists()
&& !to_create.is_dir()
{
return Err(InstallError::NotADirectory(
to_create_original.to_path_buf(),
)
.into());
}
return Err(InstallError::CreateDirFailed(
to_create_original.to_path_buf(),
e,
)
.into());
return Err(InstallError::CreateDirFailed(e.path, e.error).into());
}
}
}
Expand Down
Loading
Loading