| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2017 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
| 5 | */ |
| 6 | |
| 7 | #include "filehelper.h" |
| 8 | |
| 9 | #include <cerrno> |
| 10 | #include <fcntl.h> |
| 11 | #include <grp.h> |
| 12 | #include <libgen.h> |
| 13 | #include <sys/stat.h> |
| 14 | #include <sys/time.h> |
| 15 | #include <sys/types.h> |
| 16 | #include <unistd.h> |
| 17 | #include <utime.h> |
| 18 | |
| 19 | #include "../file_p.h" |
| 20 | #include "fdsender.h" |
| 21 | |
| 22 | #include <KAuth/HelperSupport> |
| 23 | |
| 24 | #include <QDateTime> |
| 25 | #include <QDebug> |
| 26 | #include <QDir> |
| 27 | #include <QFileInfo> |
| 28 | #include <QScopeGuard> |
| 29 | #include <QUrl> |
| 30 | |
| 31 | #ifndef O_PATH |
| 32 | #define O_PATH O_RDONLY |
| 33 | #endif |
| 34 | |
| 35 | struct Privilege { |
| 36 | uid_t uid; |
| 37 | gid_t gid; |
| 38 | }; |
| 39 | |
| 40 | static ActionType intToActionType(int action) |
| 41 | { |
| 42 | switch (action) { |
| 43 | case 1: |
| 44 | return CHMOD; |
| 45 | case 2: |
| 46 | return CHOWN; |
| 47 | case 3: |
| 48 | return DEL; |
| 49 | case 4: |
| 50 | return MKDIR; |
| 51 | case 5: |
| 52 | return OPEN; |
| 53 | case 6: |
| 54 | return OPENDIR; |
| 55 | case 7: |
| 56 | return RENAME; |
| 57 | case 8: |
| 58 | return RMDIR; |
| 59 | case 9: |
| 60 | return SYMLINK; |
| 61 | case 10: |
| 62 | return UTIME; |
| 63 | default: |
| 64 | return UNKNOWN; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | static bool sendFileDescriptor(int fd, const char *socketPath) |
| 69 | { |
| 70 | FdSender fdSender(socketPath); |
| 71 | if (fdSender.isConnected() && fdSender.sendFileDescriptor(fd)) { |
| 72 | return true; |
| 73 | } |
| 74 | return false; |
| 75 | } |
| 76 | |
| 77 | static Privilege *getTargetPrivilege(int target_fd) |
| 78 | { |
| 79 | struct stat buf; |
| 80 | if (fstat(fd: target_fd, buf: &buf) == -1) { |
| 81 | return nullptr; |
| 82 | } |
| 83 | return new Privilege{.uid: buf.st_uid, .gid: buf.st_gid}; |
| 84 | } |
| 85 | |
| 86 | static bool dropPrivilege(Privilege *p) |
| 87 | { |
| 88 | if (!p) { |
| 89 | return false; |
| 90 | } |
| 91 | |
| 92 | uid_t newuid = p->uid; |
| 93 | gid_t newgid = p->gid; |
| 94 | |
| 95 | // drop ancillary groups first because it requires root privileges. |
| 96 | if (setgroups(n: 1, groups: &newgid) == -1) { |
| 97 | return false; |
| 98 | } |
| 99 | // change effective gid and uid. |
| 100 | if (setegid(newgid) == -1 || seteuid(newuid) == -1) { |
| 101 | return false; |
| 102 | } |
| 103 | |
| 104 | return true; |
| 105 | } |
| 106 | |
| 107 | static void gainPrivilege(Privilege *p) |
| 108 | { |
| 109 | if (!p) { |
| 110 | return; |
| 111 | } |
| 112 | |
| 113 | uid_t olduid = p->uid; |
| 114 | gid_t oldgid = p->gid; |
| 115 | |
| 116 | seteuid(olduid); |
| 117 | setegid(oldgid); |
| 118 | setgroups(n: 1, groups: &oldgid); |
| 119 | } |
| 120 | |
| 121 | ActionReply FileHelper::exec(const QVariantMap &args) |
| 122 | { |
| 123 | ActionReply reply; |
| 124 | QByteArray data = args[QStringLiteral("arguments" )].toByteArray(); |
| 125 | QDataStream in(data); |
| 126 | int act; |
| 127 | QVariant arg1; |
| 128 | QVariant arg2; |
| 129 | QVariant arg3; |
| 130 | QVariant arg4; |
| 131 | in >> act >> arg1 >> arg2 >> arg3 >> arg4; // act=action, arg1=source file, arg$n=dest file, mode, uid, gid, etc. |
| 132 | ActionType action = intToActionType(action: act); |
| 133 | |
| 134 | // chown requires privilege (CAP_CHOWN) to change user but the group can be changed without it. |
| 135 | // It's much simpler to do it in one privileged call. |
| 136 | if (action == CHOWN) { |
| 137 | if (lchown(file: arg1.toByteArray().constData(), owner: arg2.toInt(), group: arg3.toInt()) == -1) { |
| 138 | reply.setError(errno); |
| 139 | } |
| 140 | return reply; |
| 141 | } |
| 142 | |
| 143 | QByteArray tempPath1; |
| 144 | QByteArray tempPath2; |
| 145 | tempPath1 = tempPath2 = arg1.toByteArray(); |
| 146 | const QByteArray parentDir = dirname(path: tempPath1.data()); |
| 147 | const QByteArray baseName = basename(path: tempPath2.data()); |
| 148 | int parent_fd = -1; |
| 149 | int base_fd = -1; |
| 150 | |
| 151 | if ((parent_fd = open(file: parentDir.data(), O_DIRECTORY | O_PATH | O_NOFOLLOW)) == -1) { |
| 152 | reply.setError(errno); |
| 153 | return reply; |
| 154 | } |
| 155 | auto parentFdCleanup = qScopeGuard(f: [parent_fd]() { |
| 156 | close(fd: parent_fd); |
| 157 | }); |
| 158 | |
| 159 | Privilege origPrivilege{.uid: geteuid(), .gid: getegid()}; |
| 160 | std::unique_ptr<Privilege> targetPrivilege; |
| 161 | |
| 162 | if (action != CHMOD && action != UTIME) { |
| 163 | targetPrivilege.reset(p: getTargetPrivilege(target_fd: parent_fd)); |
| 164 | } else { |
| 165 | if ((base_fd = openat(fd: parent_fd, file: baseName.data(), O_NOFOLLOW)) != -1) { |
| 166 | targetPrivilege.reset(p: getTargetPrivilege(target_fd: base_fd)); |
| 167 | } else { |
| 168 | reply.setError(errno); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | auto baseFdCleanup = qScopeGuard(f: [base_fd]() { |
| 173 | if (base_fd != -1) { |
| 174 | close(fd: base_fd); |
| 175 | } |
| 176 | }); |
| 177 | |
| 178 | if (dropPrivilege(p: targetPrivilege.get())) { |
| 179 | auto privilegeRestore = qScopeGuard(f: [&origPrivilege]() { |
| 180 | gainPrivilege(p: &origPrivilege); |
| 181 | }); |
| 182 | |
| 183 | switch (action) { |
| 184 | case CHMOD: { |
| 185 | int mode = arg2.toInt(); |
| 186 | if (fchmod(fd: base_fd, mode: mode) == -1) { |
| 187 | reply.setError(errno); |
| 188 | } |
| 189 | break; |
| 190 | } |
| 191 | |
| 192 | case DEL: |
| 193 | case RMDIR: { |
| 194 | int flags = 0; |
| 195 | if (action == RMDIR) { |
| 196 | flags |= AT_REMOVEDIR; |
| 197 | } |
| 198 | if (unlinkat(fd: parent_fd, name: baseName.data(), flag: flags) == -1) { |
| 199 | reply.setError(errno); |
| 200 | } |
| 201 | break; |
| 202 | } |
| 203 | |
| 204 | case MKDIR: { |
| 205 | int mode = arg2.toInt(); |
| 206 | if (mkdirat(fd: parent_fd, path: baseName.data(), mode: mode) == -1) { |
| 207 | reply.setError(errno); |
| 208 | } |
| 209 | break; |
| 210 | } |
| 211 | |
| 212 | case OPEN: |
| 213 | case OPENDIR: { |
| 214 | int oflags = arg2.toInt(); |
| 215 | int mode = arg3.toInt(); |
| 216 | int = O_NOFOLLOW; |
| 217 | if (action == OPENDIR) { |
| 218 | extraFlag |= O_DIRECTORY; |
| 219 | } |
| 220 | if (int fd = openat(fd: parent_fd, file: baseName.data(), oflag: oflags | extraFlag, mode) != -1) { |
| 221 | gainPrivilege(p: &origPrivilege); |
| 222 | if (!sendFileDescriptor(fd, socketPath: arg4.toByteArray().constData())) { |
| 223 | reply.setError(errno); |
| 224 | } |
| 225 | } else { |
| 226 | reply.setError(errno); |
| 227 | } |
| 228 | break; |
| 229 | } |
| 230 | |
| 231 | case RENAME: { |
| 232 | tempPath1 = tempPath2 = arg2.toByteArray(); |
| 233 | const QByteArray newParentDir = dirname(path: tempPath1.data()); |
| 234 | const QByteArray newBaseName = basename(path: tempPath2.data()); |
| 235 | int new_parent_fd = open(file: newParentDir.constData(), O_DIRECTORY | O_PATH | O_NOFOLLOW); |
| 236 | if (renameat(oldfd: parent_fd, old: baseName.data(), newfd: new_parent_fd, new: newBaseName.constData()) == -1) { |
| 237 | reply.setError(errno); |
| 238 | } |
| 239 | close(fd: new_parent_fd); |
| 240 | break; |
| 241 | } |
| 242 | |
| 243 | case SYMLINK: { |
| 244 | const QByteArray target = arg2.toByteArray(); |
| 245 | if (symlinkat(from: target.data(), tofd: parent_fd, to: baseName.data()) == -1) { |
| 246 | reply.setError(errno); |
| 247 | } |
| 248 | break; |
| 249 | } |
| 250 | |
| 251 | case UTIME: { |
| 252 | timespec times[2]; |
| 253 | time_t actime = arg2.toULongLong(); |
| 254 | time_t modtime = arg3.toULongLong(); |
| 255 | times[0].tv_sec = actime / 1000; |
| 256 | times[0].tv_nsec = actime * 1000; |
| 257 | times[1].tv_sec = modtime / 1000; |
| 258 | times[1].tv_nsec = modtime * 1000; |
| 259 | if (futimens(fd: base_fd, times: times) == -1) { |
| 260 | reply.setError(errno); |
| 261 | } |
| 262 | break; |
| 263 | } |
| 264 | |
| 265 | default: |
| 266 | reply.setError(ENOTSUP); |
| 267 | break; |
| 268 | } |
| 269 | } else { |
| 270 | reply.setError(errno); |
| 271 | } |
| 272 | return reply; |
| 273 | } |
| 274 | |
| 275 | KAUTH_HELPER_MAIN("org.kde.kio.file" , FileHelper) |
| 276 | |
| 277 | #include "moc_filehelper.cpp" |
| 278 | |