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
35struct Privilege {
36 uid_t uid;
37 gid_t gid;
38};
39
40static 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
68static 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
77static 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
86static 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
107static 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
121ActionReply 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 extraFlag = 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
275KAUTH_HELPER_MAIN("org.kde.kio.file", FileHelper)
276
277#include "moc_filehelper.cpp"
278

source code of kio/src/kioworkers/file/kauth/filehelper.cpp