diff --git a/src/ioslaves/file/file.h b/src/ioslaves/file/file.h --- a/src/ioslaves/file/file.h +++ b/src/ioslaves/file/file.h @@ -106,6 +106,13 @@ PrivilegeOperationReturnValue execWithElevatedPrivilege(ActionType action, const QVariant &arg1, const QVariant &arg2 = QVariant(), const QVariant &arg3 = QVariant()); + PrivilegeOperationReturnValue tryOpen(QFile &f, const QByteArray &path, int flags, int mode); + + // We want to execute chmod/chown/utime with elevated privileges (in copy & put) + // only during the brief period privileges are elevated. If it's not the case show + // a warning and continue. + PrivilegeOperationReturnValue tryChangeFileAttr(ActionType action, const QVariant &arg1, + const QVariant &arg2, const QVariant &arg3 = QVariant()); private: mutable QHash mUsercache; mutable QHash mGroupcache; diff --git a/src/ioslaves/file/file.cpp b/src/ioslaves/file/file.cpp --- a/src/ioslaves/file/file.cpp +++ b/src/ioslaves/file/file.cpp @@ -77,6 +77,8 @@ #include #include +#include "fdreceiver.h" + Q_LOGGING_CATEGORY(KIO_FILE, "kf5.kio.kio_file") // Pseudo plugin class to embed meta data @@ -234,26 +236,30 @@ (setACL(_path.data(), permissions, false) == -1) || /* if not a directory, cannot set default ACLs */ (setACL(_path.data(), permissions, true) == -1 && errno != ENOTDIR)) { - - switch (errno) { - case EPERM: - case EACCES: - error(KIO::ERR_ACCESS_DENIED, path); - break; -#if defined(ENOTSUP) - case ENOTSUP: // from setACL since chmod can't return ENOTSUP - error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path)); - break; -#endif - case ENOSPC: - error(KIO::ERR_DISK_FULL, path); - break; - default: - error(KIO::ERR_CANNOT_CHMOD, path); + if (auto err = execWithElevatedPrivilege(CHMOD, _path, permissions)) { + if (!err.wasCanceled()) { + switch (errno) { + case EPERM: + case EACCES: + error(KIO::ERR_ACCESS_DENIED, path); + break; + #if defined(ENOTSUP) + case ENOTSUP: // from setACL since chmod can't return ENOTSUP + error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path)); + break; + #endif + case ENOSPC: + error(KIO::ERR_DISK_FULL, path); + break; + default: + error(KIO::ERR_CANNOT_CHMOD, path); + } + return; + } } - } else { - finished(); } + + finished(); } void FileProtocol::setModificationTime(const QUrl &url, const QDateTime &mtime) @@ -265,8 +271,12 @@ utbuf.actime = statbuf.st_atime; // access time, unchanged utbuf.modtime = mtime.toTime_t(); // modification time if (::utime(QFile::encodeName(path).constData(), &utbuf) != 0) { - // TODO: errno could be EACCES, EPERM, EROFS - error(KIO::ERR_CANNOT_SETTIME, path); + if (auto err = execWithElevatedPrivilege(UTIME, path, qint64(utbuf.actime), qint64(utbuf.modtime))) { + if (!err.wasCanceled()) { + // TODO: errno could be EACCES, EPERM, EROFS + error(KIO::ERR_CANNOT_SETTIME, path); + } + } } else { finished(); } @@ -283,16 +293,26 @@ // Remove existing file or symlink, if requested (#151851) if (metaData(QStringLiteral("overwrite")) == QLatin1String("true")) { - QFile::remove(path); + if (!QFile::remove(path)) { + execWithElevatedPrivilege(DEL, path); + } } QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(path).constData(), &buff) == -1) { - if (!QDir().mkdir(path)) { - //TODO: add access denied & disk full (or another reasons) handling (into Qt, possibly) - error(KIO::ERR_CANNOT_MKDIR, path); - return; - } else { + bool dirCreated; + if (!(dirCreated = QDir().mkdir(path))) { + if (auto err = execWithElevatedPrivilege(MKDIR, path)) { + if (!err.wasCanceled()) { + //TODO: add access denied & disk full (or another reasons) handling (into Qt, possibly) + error(KIO::ERR_CANNOT_MKDIR, path); + } + return; + } + dirCreated = true; + } + + if (dirCreated) { if (permissions != -1) { chmod(url, permissions); } else { @@ -343,8 +363,12 @@ QFile f(path); if (!f.open(QIODevice::ReadOnly)) { - error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); - return; + if (auto err = tryOpen(f, QFile::encodeName(path), O_RDONLY, S_IRUSR)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); + } + return; + } } #if HAVE_FADVISE @@ -538,6 +562,11 @@ void FileProtocol::put(const QUrl &url, int _mode, KIO::JobFlags _flags) { + if (privilegeOperationUnitTestMode()) { + finished(); + return; + } + const QString dest_orig = url.toLocalFile(); // qDebug() << dest_orig << "mode=" << _mode; @@ -619,15 +648,36 @@ } if (!f.isOpen()) { - // qDebug() << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode; - // qDebug() << "QFile error==" << f.error() << "(" << f.errorString() << ")"; + int oflags = 0; + int filemode = _mode; - if (f.error() == QFileDevice::PermissionsError) { - error(KIO::ERR_WRITE_ACCESS_DENIED, dest); + if ((_flags & KIO::Resume)) { + oflags = O_RDWR | O_APPEND; } else { - error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + oflags = O_WRONLY | O_TRUNC | O_CREAT; + if (_mode != -1) { + filemode = _mode | S_IWUSR | S_IRUSR; + } + } + + if (auto err = tryOpen(f, QFile::encodeName(dest), oflags, filemode)) { + if (!err.wasCanceled()) { + // qDebug() << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode; + // qDebug() << "QFile error==" << f.error() << "(" << f.errorString() << ")"; + + if (f.error() == QFileDevice::PermissionsError) { + error(KIO::ERR_WRITE_ACCESS_DENIED, dest); + } else { + error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + } + } + return; + } else { + if ((_flags & KIO::Resume)) { + execWithElevatedPrivilege(CHOWN, dest, getuid(), getgid()); + QFile::setPermissions(dest, modeToQFilePermissions(filemode)); + } } - return; } } @@ -681,23 +731,28 @@ //QFile::rename() never overwrites the destination file unlike ::remove, //so we must remove it manually first if (_flags & KIO::Overwrite) { - QFile::remove(dest_orig); + if (!QFile::remove(dest_orig)) { + execWithElevatedPrivilege(DEL, dest_orig); + } } if (!QFile::rename(dest, dest_orig)) { - qCWarning(KIO_FILE) << " Couldn't rename " << dest << " to " << dest_orig; - error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); - return; + if (auto err = execWithElevatedPrivilege(RENAME, dest, dest_orig)) { + if (!err.wasCanceled()) { + qCWarning(KIO_FILE) << " Couldn't rename " << dest << " to " << dest_orig; + error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); + } + return; + } } org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(dest), QUrl::fromLocalFile(dest_orig)); } // set final permissions if (_mode != -1 && !(_flags & KIO::Resume)) { if (!QFile::setPermissions(dest_orig, modeToQFilePermissions(_mode))) { // couldn't chmod. Eat the error if the filesystem apparently doesn't support it. - KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest_orig); - if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { + if (tryChangeFileAttr(CHMOD, dest_orig, _mode)) { warning(i18n("Could not change permissions for\n%1", dest_orig)); } } @@ -723,7 +778,9 @@ struct utimbuf utbuf; utbuf.actime = dest_statbuf.st_atime; utbuf.modtime = dt.toTime_t(); - utime(QFile::encodeName(dest_orig).constData(), &utbuf); + if (utime(QFile::encodeName(dest_orig).constData(), &utbuf) != 0) { + tryChangeFileAttr(UTIME, dest_orig, qint64(utbuf.actime), qint64(utbuf.modtime)); + } #endif } } @@ -1335,17 +1392,25 @@ } else { //qDebug() << "QFile::remove" << itemPath; if (!QFile::remove(itemPath)) { - error(KIO::ERR_CANNOT_DELETE, itemPath); - return false; + if (auto err = execWithElevatedPrivilege(DEL, itemPath)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE, itemPath); + } + return false; + } } } } QDir dir; Q_FOREACH (const QString &itemPath, dirsToDelete) { //qDebug() << "QDir::rmdir" << itemPath; if (!dir.rmdir(itemPath)) { - error(KIO::ERR_CANNOT_DELETE, itemPath); - return false; + if (auto err = execWithElevatedPrivilege(RMDIR, itemPath)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE, itemPath); + } + return false; + } } } return true; diff --git a/src/ioslaves/file/file_unix.cpp b/src/ioslaves/file/file_unix.cpp --- a/src/ioslaves/file/file_unix.cpp +++ b/src/ioslaves/file/file_unix.cpp @@ -41,6 +41,8 @@ #include +#include "fdreceiver.h" + //sendfile has different semantics in different platforms #if defined HAVE_SENDFILE && defined Q_OS_LINUX #define USE_SENDFILE 1 @@ -76,9 +78,60 @@ && (requestPrivilegeOperation() == KIO::OperationAllowed); } +PrivilegeOperationReturnValue FileProtocol::tryOpen(QFile &f, const QByteArray &path, int flags, int mode) +{ + FdReceiver fdRecv(socketPath()); + if (!fdRecv.isListening()) { + return PrivilegeOperationReturnValue::failure(); + } + + QIODevice::OpenMode openMode; + if (flags & O_RDONLY) { + openMode |= QIODevice::ReadOnly; + } + if (flags & O_WRONLY || flags & O_CREAT) { + openMode |= QIODevice::WriteOnly; + } + if (flags & O_RDWR) { + openMode |= QIODevice::ReadWrite; + } + if (flags & O_TRUNC) { + openMode |= QIODevice::Truncate; + } + if (flags & O_APPEND) { + openMode |= QIODevice::Append; + } + + if (auto err = execWithElevatedPrivilege(OPEN, path, flags, mode)) { + return err; + } else { + int fd = fdRecv.fileDescriptor(); + if (fd < 3 || !f.open(fd, openMode, QFileDevice::AutoCloseHandle)) { + return PrivilegeOperationReturnValue::failure(); + } + } + return PrivilegeOperationReturnValue::success(); +} + +PrivilegeOperationReturnValue FileProtocol::tryChangeFileAttr(ActionType action, const QVariant &arg1, + const QVariant &arg2, const QVariant &arg3) +{ + KAuth::Action execAction(QStringLiteral("org.kde.kio.file.exec")); + execAction.setHelperId(QStringLiteral("org.kde.kio.file")); + if (execAction.status() == KAuth::Action::AuthorizedStatus) { + return execWithElevatedPrivilege(action, arg1, arg2, arg3); + } + return PrivilegeOperationReturnValue::failure(); +} + void FileProtocol::copy(const QUrl &srcUrl, const QUrl &destUrl, int _mode, JobFlags _flags) { + if (privilegeOperationUnitTestMode()) { + finished(); + return; + } + // qDebug() << "copy(): " << srcUrl << " -> " << destUrl << ", mode=" << _mode; const QString src = srcUrl.toLocalFile(); @@ -131,34 +184,60 @@ // the symlink actually points to current source! if ((_flags & KIO::Overwrite) && ((buff_dest.st_mode & QT_STAT_MASK) == QT_STAT_LNK)) { //qDebug() << "copy(): LINK DESTINATION"; - QFile::remove(dest); + if (!QFile::remove(dest)) { + if (auto err = execWithElevatedPrivilege(DEL, _dest)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE_ORIGINAL, dest); + } + return; + } + } } } QFile src_file(src); if (!src_file.open(QIODevice::ReadOnly)) { - error(KIO::ERR_CANNOT_OPEN_FOR_READING, src); - return; + if (auto err = tryOpen(src_file, _src, O_RDONLY, S_IRUSR)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_OPEN_FOR_READING, src); + } + return; + } } #if HAVE_FADVISE posix_fadvise(src_file.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); #endif QFile dest_file(dest); if (!dest_file.open(QIODevice::Truncate | QIODevice::WriteOnly)) { - // qDebug() << "###### COULD NOT WRITE " << dest; - if (errno == EACCES) { - error(KIO::ERR_WRITE_ACCESS_DENIED, dest); - } else { - error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + if (auto err = tryOpen(dest_file, _dest, O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR)) { + if (!err.wasCanceled()) { + // qDebug() << "###### COULD NOT WRITE " << dest; + if (errno == EACCES) { + error(KIO::ERR_WRITE_ACCESS_DENIED, dest); + } else { + error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); + } + } + src_file.close(); + return; } - src_file.close(); - return; } // nobody shall be allowed to peek into the file during creation - dest_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); + if (!QFile::setPermissions(dest, QFileDevice::ReadOwner | QFileDevice::WriteOwner)) { + if (auto err = execWithElevatedPrivilege(CHOWN, _dest, getuid(), getgid())) { + dest_file.close(); + execWithElevatedPrivilege(DEL, _dest); + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_CHOWN, dest); + } + return; + } else { + QFile::setPermissions(dest, QFileDevice::ReadOwner | QFileDevice::WriteOwner); + } + } #if HAVE_FADVISE posix_fadvise(dest_file.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); @@ -219,7 +298,9 @@ acl_free(acl); } #endif - dest_file.remove(); // don't keep partly copied file + if (!QFile::remove(dest)) { // don't keep partly copied file + execWithElevatedPrivilege(DEL, _dest); + } return; } if (n == 0) { @@ -240,7 +321,9 @@ acl_free(acl); } #endif - dest_file.remove(); // don't keep partly copied file + if (!QFile::remove(dest)) { // don't keep partly copied file + execWithElevatedPrivilege(DEL, _dest); + } return; } processed_size += n; @@ -261,7 +344,9 @@ acl_free(acl); } #endif - dest_file.remove(); // don't keep partly copied file + if (!QFile::remove(dest)) { // don't keep partly copied file + execWithElevatedPrivilege(DEL, _dest); + } return; } @@ -276,9 +361,7 @@ || (acl && acl_set_file(_dest.data(), ACL_TYPE_ACCESS, acl) != 0) #endif ) { - KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest); - // Eat the error if the filesystem apparently doesn't support chmod. - if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { + if (tryChangeFileAttr(CHMOD, _dest, _mode)) { warning(i18n("Could not change permissions for '%1'", dest)); } } @@ -294,15 +377,19 @@ // we might not be allowed to change the owner (void)::chown(_dest.data(), buff_src.st_uid, -1 /*keep group*/); } else { - qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve group for '%1'").arg(dest); + if (tryChangeFileAttr(CHOWN, _dest, buff_src.st_uid, buff_src.st_gid)) { + qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve group for '%1'").arg(dest); + } } // copy access and modification time struct utimbuf ut; ut.actime = buff_src.st_atime; ut.modtime = buff_src.st_mtime; if (::utime(_dest.data(), &ut) != 0) { - qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve access and modification time for '%1'").arg(dest); + if (tryChangeFileAttr(UTIME, _dest, qint64(ut.actime), qint64(ut.modtime))) { + qCWarning(KIO_FILE) << QStringLiteral("Couldn't preserve access and modification time for '%1'").arg(dest); + } } processedSize(buff_src.st_size); @@ -477,16 +564,20 @@ } if (::rename(_src.data(), _dest.data())) { - if ((errno == EACCES) || (errno == EPERM)) { - error(KIO::ERR_ACCESS_DENIED, dest); - } else if (errno == EXDEV) { - error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename")); - } else if (errno == EROFS) { // The file is on a read-only filesystem - error(KIO::ERR_CANNOT_DELETE, src); - } else { - error(KIO::ERR_CANNOT_RENAME, src); + if (auto err = execWithElevatedPrivilege(RENAME, _src, _dest)) { + if (!err.wasCanceled()) { + if ((errno == EACCES) || (errno == EPERM)) { + error(KIO::ERR_ACCESS_DENIED, dest); + } else if (errno == EXDEV) { + error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename")); + } else if (errno == EROFS) { // The file is on a read-only filesystem + error(KIO::ERR_CANNOT_DELETE, src); + } else { + error(KIO::ERR_CANNOT_RENAME, src); + } + } + return; } - return; } finished(); @@ -502,8 +593,12 @@ if ((flags & KIO::Overwrite)) { // Try to delete the destination if (unlink(QFile::encodeName(dest).constData()) != 0) { - error(KIO::ERR_CANNOT_DELETE, dest); - return; + if (auto err = execWithElevatedPrivilege(DEL, dest)) { + if (!err.wasCanceled()) { + error(KIO::ERR_CANNOT_DELETE, dest); + } + return; + } } // Try again - this won't loop forever since unlink succeeded symlink(target, destUrl, flags); @@ -518,9 +613,13 @@ return; } } else { - // Some error occurred while we tried to symlink - error(KIO::ERR_CANNOT_SYMLINK, dest); - return; + if (auto err = execWithElevatedPrivilege(SYMLINK, dest, target)) { + if (!err.wasCanceled()) { + // Some error occurred while we tried to symlink + error(KIO::ERR_CANNOT_SYMLINK, dest); + } + return; + } } } finished(); @@ -538,14 +637,18 @@ // qDebug() << "Deleting file "<< url; if (unlink(_path.data()) == -1) { - if ((errno == EACCES) || (errno == EPERM)) { - error(KIO::ERR_ACCESS_DENIED, path); - } else if (errno == EISDIR) { - error(KIO::ERR_IS_DIRECTORY, path); - } else { - error(KIO::ERR_CANNOT_DELETE, path); + if (auto err = execWithElevatedPrivilege(DEL, _path)) { + if (!err.wasCanceled()) { + if ((errno == EACCES) || (errno == EPERM)) { + error(KIO::ERR_ACCESS_DENIED, path); + } else if (errno == EISDIR) { + error(KIO::ERR_IS_DIRECTORY, path); + } else { + error(KIO::ERR_CANNOT_DELETE, path); + } + } + return; } - return; } } else { @@ -560,11 +663,15 @@ } } if (QT_RMDIR(_path.data()) == -1) { - if ((errno == EACCES) || (errno == EPERM)) { - error(KIO::ERR_ACCESS_DENIED, path); - } else { - // qDebug() << "could not rmdir " << perror; - error(KIO::ERR_CANNOT_RMDIR, path); + if (auto err = execWithElevatedPrivilege(RMDIR, _path)) { + if (!err.wasCanceled()) { + if ((errno == EACCES) || (errno == EPERM)) { + error(KIO::ERR_ACCESS_DENIED, path); + } else { + // qDebug() << "could not rmdir " << perror; + error(KIO::ERR_CANNOT_RMDIR, path); + } + } return; } } @@ -607,16 +714,20 @@ } if (::chown(_path.constData(), uid, gid) == -1) { - switch (errno) { - case EPERM: - case EACCES: - error(KIO::ERR_ACCESS_DENIED, path); - break; - case ENOSPC: - error(KIO::ERR_DISK_FULL, path); - break; - default: - error(KIO::ERR_CANNOT_CHOWN, path); + if (auto err = execWithElevatedPrivilege(CHOWN, _path, uid, gid)) { + if (!err.wasCanceled()) { + switch (errno) { + case EPERM: + case EACCES: + error(KIO::ERR_ACCESS_DENIED, path); + break; + case ENOSPC: + error(KIO::ERR_DISK_FULL, path); + break; + default: + error(KIO::ERR_CANNOT_CHOWN, path); + } + } } } else { finished(); diff --git a/src/ioslaves/file/file_win.cpp b/src/ioslaves/file/file_win.cpp --- a/src/ioslaves/file/file_win.cpp +++ b/src/ioslaves/file/file_win.cpp @@ -392,3 +392,12 @@ { return PrivilegeOperationReturnValue::failure(); } +PrivilegeOperationReturnValue tryOpen(QFile &f, const QByteArray &, int , int) +{ + return PrivilegeOperationReturnValue::failure(); +} + +PrivilegeOperationReturnValue tryChangeFileAttr(ActionType , const QVariant &, const QVariant &, const QVariant &) +{ + return PrivilegeOperationReturnValue::failure(); +} diff --git a/src/ioslaves/file/kauth/filehelper.cpp b/src/ioslaves/file/kauth/filehelper.cpp --- a/src/ioslaves/file/kauth/filehelper.cpp +++ b/src/ioslaves/file/kauth/filehelper.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "filehelper.h" #include "fdsender.h"