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