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 | |