Changeset View
Standalone View
src/ioslaves/file/kauth/filehelper.cpp
Show All 15 Lines | 1 | /*** | |||
---|---|---|---|---|---|
16 | 16 | | |||
17 | You should have received a copy of the GNU Lesser General Public | 17 | You should have received a copy of the GNU Lesser General Public | ||
18 | License along with this library. If not, see <http://www.gnu.org/licenses/>. | 18 | License along with this library. If not, see <http://www.gnu.org/licenses/>. | ||
19 | ***/ | 19 | ***/ | ||
20 | 20 | | |||
21 | #include <utime.h> | 21 | #include <utime.h> | ||
22 | #include <fcntl.h> | 22 | #include <fcntl.h> | ||
23 | #include <unistd.h> | 23 | #include <unistd.h> | ||
24 | #include <dirent.h> | | |||
25 | #include <sys/stat.h> | | |||
26 | #include <errno.h> | 24 | #include <errno.h> | ||
25 | #include <libgen.h> | ||||
26 | #include <grp.h> | ||||
27 | #include <sys/stat.h> | ||||
28 | #include <sys/time.h> | ||||
27 | 29 | | |||
28 | #include "filehelper.h" | 30 | #include "filehelper.h" | ||
29 | #include "fdsender.h" | 31 | #include "fdsender.h" | ||
30 | #include "../file_p.h" | 32 | #include "../file_p.h" | ||
31 | 33 | | |||
34 | struct OwnerId { | ||||
35 | uid_t uid; | ||||
36 | gid_t gid; | ||||
37 | }; | ||||
38 | | ||||
39 | static ActionType intToActionType(int action) | ||||
40 | { | ||||
41 | switch (action) { | ||||
42 | case 1: return CHMOD; | ||||
43 | case 2: return CHOWN; | ||||
44 | case 3: return DEL; | ||||
45 | case 4: return MKDIR; | ||||
46 | case 5: return OPEN; | ||||
47 | case 6: return OPENDIR; | ||||
48 | case 7: return RENAME; | ||||
49 | case 8: return RMDIR; | ||||
50 | case 9: return SYMLINK; | ||||
51 | case 10: return UTIME; | ||||
52 | default: return UNKNOWN; | ||||
53 | } | ||||
54 | } | ||||
55 | | ||||
32 | static bool sendFileDescriptor(int fd, const char *socketPath) | 56 | static bool sendFileDescriptor(int fd, const char *socketPath) | ||
33 | { | 57 | { | ||
34 | FdSender fdSender(socketPath); | 58 | FdSender fdSender(socketPath); | ||
35 | if (fdSender.isConnected() && fdSender.sendFileDescriptor(fd)) { | 59 | if (fdSender.isConnected() && fdSender.sendFileDescriptor(fd)) { | ||
36 | return true; | 60 | return true; | ||
37 | } | 61 | } | ||
38 | return false; | 62 | return false; | ||
39 | } | 63 | } | ||
40 | 64 | | |||
65 | static OwnerId *getNecessaryPrivileges(ActionType action, int parent_fd, const QByteArray &baseName) | ||||
66 | { | ||||
67 | | ||||
68 | //for chmod, chown, utime, drop privileges to that of owner of the target file/dir | ||||
69 | //for other actions use owner of the parent directory. | ||||
70 | int target_fd = -1; | ||||
71 | if (action != CHMOD && action != CHOWN && action != UTIME) { | ||||
72 | target_fd = parent_fd; | ||||
73 | } else { | ||||
74 | target_fd = openat(parent_fd, baseName.data(), O_NOFOLLOW); | ||||
maltek: This is still subject to race conditions. You're opening the file with O_NOFOLLOW and calling… | |||||
75 | } | ||||
76 | | ||||
77 | struct stat buf = {0}; | ||||
78 | if (fstat(target_fd, &buf) == -1) { | ||||
79 | return nullptr; | ||||
80 | } | ||||
81 | | ||||
82 | return new OwnerId{buf.st_uid, buf.st_gid}; | ||||
83 | } | ||||
84 | | ||||
85 | static bool dropPrivileges(OwnerId *o) | ||||
86 | { | ||||
87 | if (!o) { | ||||
88 | return false; | ||||
89 | } | ||||
90 | | ||||
91 | uid_t newuid = o->uid; | ||||
92 | gid_t newgid = o->gid; | ||||
93 | delete o; | ||||
94 | | ||||
95 | if (newuid == 0 || newgid == 0) { | ||||
96 | return false; | ||||
97 | } | ||||
98 | //drop ancillary groups first because it requires root privileges. | ||||
99 | if (setgroups(1, &newgid) == -1) { | ||||
100 | return false; | ||||
101 | } | ||||
102 | //change real, effective, saved gid and uid. | ||||
103 | if (setregid(newgid, newgid) == -1 || setreuid(newuid, newuid) == -1) { | ||||
104 | return false; | ||||
105 | } | ||||
After this return false, the process is in some undefined state with unknown groups, since there is no attempt to restore the original groups. The chance of these calls failing are quite slim, however - it can only really happen due to programming error when the program doesn't have privileges to call sete[ug]id. Personally, I'd just abort the program in such circumstances, but since it should never be happening, reasonable people might disagree. maltek: After this `return false`, the process is in some undefined state with unknown groups, since… | |||||
106 | | ||||
107 | return true; | ||||
108 | } | ||||
109 | | ||||
41 | ActionReply FileHelper::exec(const QVariantMap &args) | 110 | ActionReply FileHelper::exec(const QVariantMap &args) | ||
42 | { | 111 | { | ||
43 | ActionReply reply; | 112 | ActionReply reply; | ||
44 | QByteArray data = args["arguments"].toByteArray(); | 113 | QByteArray data = args["arguments"].toByteArray(); | ||
45 | QDataStream in(data); | 114 | QDataStream in(data); | ||
46 | int action; | 115 | QVariant act, arg1, arg2, arg3, arg4; | ||
47 | QVariant arg1, arg2, arg3, arg4; | 116 | in >> act >> arg1 >> arg2 >> arg3 >> arg4; // act=action, arg1=source file, arg$n=dest file, mode, uid, gid, etc. | ||
48 | in >> action >> arg1 >> arg2 >> arg3 >> arg4; | | |||
49 | 117 | | |||
50 | // the path of an existing or a new file/dir upon which the method will operate | 118 | ActionType action = intToActionType(act.toInt()); | ||
51 | const QByteArray path = arg1.toByteArray(); | 119 | QByteArray tempPath1, tempPath2; | ||
120 | tempPath1 = tempPath2 = arg1.toByteArray(); | ||||
121 | const QByteArray parentDir = dirname(tempPath1.data()); | ||||
122 | const QByteArray baseName = basename(tempPath2.data()); | ||||
maltek: This needs error handling. | |||||
123 | int parent_fd = open(parentDir.data(), O_DIRECTORY | O_PATH | O_NOFOLLOW); | ||||
124 | | ||||
125 | int new_parent_fd = -1; | ||||
126 | QByteArray newParentDir, newBaseName; | ||||
127 | if (action == RENAME) { | ||||
128 | tempPath1 = tempPath2 = arg2.toByteArray(); | ||||
129 | newParentDir = dirname(tempPath1.data()); | ||||
130 | newBaseName = basename(tempPath2.data()); | ||||
131 | new_parent_fd = open(newParentDir.constData(), O_DIRECTORY | O_PATH | O_NOFOLLOW); | ||||
132 | } | ||||
133 | | ||||
134 | OwnerId *oid = getNecessaryPrivileges(action, parent_fd, baseName); | ||||
135 | if (!dropPrivileges(oid)) { | ||||
136 | reply.setError(errno); | ||||
137 | } | ||||
52 | 138 | | |||
53 | switch(action) { | 139 | switch(action) { | ||
54 | case CHMOD: { | 140 | case CHMOD: { | ||
55 | int mode = arg2.toInt(); | 141 | int mode = arg2.toInt(); | ||
56 | if (chmod(path.data(), mode) == 0) { | 142 | if (fchmodat(parent_fd, baseName.data(), mode, AT_SYMLINK_NOFOLLOW) == -1) { | ||
AT_SYMLINK_NOFOLLOW is not implemented for fchmodat, so this will always fail with ENOTSUP. It seems the only safe way to do this is to open() the file with O_NOFOLLOW, and then use fchmod(). maltek: AT_SYMLINK_NOFOLLOW is not implemented for fchmodat, so this will always fail with ENOTSUP. It… | |||||
57 | return reply; | 143 | reply.setError(errno); | ||
58 | } | 144 | } | ||
59 | break; | 145 | break; | ||
60 | } | 146 | } | ||
147 | | ||||
61 | case CHOWN: { | 148 | case CHOWN: { | ||
62 | int uid = arg2.toInt(); | 149 | int uid = arg2.toInt(); | ||
63 | int gid = arg3.toInt(); | 150 | int gid = arg3.toInt(); | ||
64 | if (chown(path.data(), uid, gid) == 0) { | 151 | if (fchownat(parent_fd, baseName.data(), uid, gid, AT_SYMLINK_NOFOLLOW) == -1) { | ||
65 | return reply; | 152 | reply.setError(errno); | ||
66 | } | 153 | } | ||
67 | break; | 154 | break; | ||
68 | } | 155 | } | ||
69 | case DEL: { | 156 | | ||
70 | if (unlink(path.data()) == 0) { | 157 | case DEL: | ||
71 | return reply; | 158 | case RMDIR: { | ||
159 | int flags = 0; | ||||
160 | if (action == RMDIR) { | ||||
161 | flags |= AT_REMOVEDIR; | ||||
162 | } | ||||
163 | if (unlinkat(parent_fd, baseName.data(), flags) == -1) { | ||||
164 | reply.setError(errno); | ||||
72 | } | 165 | } | ||
73 | break; | 166 | break; | ||
74 | } | 167 | } | ||
168 | | ||||
75 | case MKDIR: { | 169 | case MKDIR: { | ||
76 | if (mkdir(path.data(), 0777) == 0) { | 170 | int mode = arg2.toInt(); | ||
typo: there's a second & missing after the first condition. (I don't think it ends up affecting the result.) maltek: typo: there's a second & missing after the first condition. (I don't think it ends up affecting… | |||||
77 | return reply; | 171 | if (mkdirat(parent_fd, baseName.data(), mode) == -1) { | ||
172 | reply.setError(errno); | ||||
78 | } | 173 | } | ||
There's no error handling here, which will likely lead to weird EBADF errors getting returned later. maltek: There's no error handling here, which will likely lead to weird `EBADF` errors getting returned… | |||||
79 | break; | 174 | break; | ||
For chown, dropping privileges here means that the chown later can't succeed - it's not possible to 'gift' a file to another user. I think it should be handled more like DEL/RMDIR/MKDIR etc. maltek: For `chown`, dropping privileges here means that the `chown` later can't succeed - it's not… | |||||
Ah! Since I was testing inside /opt I didn't notice. I think the order here should be: drop privilege -> change grp -> gain privilege -> change user. chinmoyr: Ah! Since I was testing inside /opt I didn't notice. I think the order here should be: drop… | |||||
IMO, it's fine (and less complicated) to just do both in one single privileged fchmod call. maltek: IMO, it's fine (and less complicated) to just do both in one single privileged `fchmod` call. | |||||
80 | } | 175 | } | ||
81 | case OPEN: { | 176 | | ||
177 | case OPEN: | ||||
178 | case OPENDIR: { | ||||
I just realized that this wouldn't allow changing the owner of symbolic links. The way to go here is lchown. maltek: I just realized that this wouldn't allow changing the owner of symbolic links. The way to go… | |||||
Do you think it'll be a bad idea to skip the case for symlinks in utime, chmod, chown, for now? Right now there's no code in KIO that requires these operations to be performed on the link itself. chinmoyr: Do you think it'll be a bad idea to skip the case for symlinks in utime, chmod, chown, for now? | |||||
Fine by me - I'm only really here to look for security problems, not to decide on which features are required for this to land. maltek: Fine by me - I'm only really here to look for security problems, not to decide on which… | |||||
82 | int oflags = arg2.toInt(); | 179 | int oflags = arg2.toInt(); | ||
83 | int mode = arg3.toInt(); | 180 | int mode = arg3.toInt(); | ||
84 | int fd = open(path.data(), oflags, mode); | 181 | int extraFlag = O_NOFOLLOW; | ||
182 | if (action == OPENDIR) { | ||||
183 | extraFlag |= O_DIRECTORY; | ||||
184 | } | ||||
185 | int fd = openat(parent_fd, baseName.data(), oflags | extraFlag, mode); | ||||
85 | bool success = (fd != -1) && sendFileDescriptor(fd, arg4.toByteArray().constData()); | 186 | bool success = (fd != -1) && sendFileDescriptor(fd, arg4.toByteArray().constData()); | ||
86 | close(fd); | 187 | close(fd); | ||
87 | if (success) { | 188 | if (!success) { | ||
88 | return reply; | 189 | reply.setError(errno); | ||
89 | } | | |||
90 | break; | | |||
91 | } | | |||
92 | case OPENDIR: { | | |||
93 | DIR *dp = opendir(path.data()); | | |||
94 | bool success = false; | | |||
95 | if (dp) { | | |||
96 | int fd = dirfd(dp); | | |||
97 | success = (fd != -1) && sendFileDescriptor(fd, arg4.toByteArray().constData()); | | |||
98 | closedir(dp); | | |||
99 | if (success) { | | |||
100 | return reply; | | |||
101 | } | | |||
102 | } | 190 | } | ||
103 | break; | 191 | break; | ||
104 | } | 192 | } | ||
193 | | ||||
105 | case RENAME: { | 194 | case RENAME: { | ||
106 | const QByteArray newName = arg2.toByteArray(); | 195 | if (renameat(parent_fd, baseName.data(), new_parent_fd, newBaseName.constData()) == -1) { | ||
107 | if (rename(path.data(), newName.data()) == 0) { | 196 | oid = getNecessaryPrivileges(action, new_parent_fd, newBaseName); | ||
108 | return reply; | 197 | if (!dropPrivileges(oid) && renameat(parent_fd, baseName.data(), new_parent_fd, newBaseName.constData()) == -1) { | ||
109 | } | 198 | reply.setError(errno); | ||
110 | break; | | |||
111 | } | 199 | } | ||
112 | case RMDIR: { | | |||
113 | if (rmdir(path.data()) == 0) { | | |||
114 | return reply; | | |||
115 | } | 200 | } | ||
116 | break; | 201 | break; | ||
117 | } | 202 | } | ||
203 | | ||||
118 | case SYMLINK: { | 204 | case SYMLINK: { | ||
119 | const QByteArray target = arg2.toByteArray(); | 205 | const QByteArray target = arg2.toByteArray(); | ||
120 | if (symlink(target.data(), path.data()) == 0) { | 206 | if (symlinkat(target.data(), parent_fd, baseName.data()) == -1) { | ||
121 | return reply; | 207 | return reply; | ||
122 | } | 208 | } | ||
123 | break; | 209 | break; | ||
124 | } | 210 | } | ||
211 | | ||||
125 | case UTIME: { | 212 | case UTIME: { | ||
126 | utimbuf ut; | 213 | timespec times[2]; | ||
127 | ut.actime = arg2.toULongLong(); | 214 | time_t actime = arg2.toULongLong(); | ||
128 | ut.modtime = arg3.toULongLong(); | 215 | time_t modtime = arg3.toULongLong(); | ||
In the error case, this attempts sending fd -1. I haven't checked the underlying code, but this will probably pollute errno with something unrelated to the underlying error. maltek: In the error case, this attempts sending fd `-1`. I haven't checked the underlying code, but… | |||||
129 | if (utime(path.data(), &ut) == 0) { | 216 | times[0].tv_sec = actime * 1000; | ||
130 | return reply; | 217 | times[0].tv_nsec = actime / 1000; | ||
218 | times[1].tv_sec = modtime * 1000; | ||||
219 | times[1].tv_nsec = modtime / 1000; | ||||
maltek: I *think* the divisions/multiplications are reversed here. | |||||
220 | if (utimensat(parent_fd, baseName.data(), times, AT_SYMLINK_NOFOLLOW) == -1) { | ||||
221 | reply.setError(errno); | ||||
131 | } | 222 | } | ||
132 | break; | 223 | break; | ||
133 | } | 224 | } | ||
134 | }; | 225 | } | ||
135 | 226 | | |||
Somewhere here, I'd expect the privileges to be escalated again, to restore the state from before the call to dropPrivileges(). maltek: Somewhere here, I'd expect the privileges to be escalated again, to restore the state from… | |||||
136 | reply.setError(errno ? errno : -1); | 227 | close(parent_fd); | ||
228 | close(new_parent_fd); | ||||
137 | return reply; | 229 | return reply; | ||
138 | } | 230 | } | ||
139 | 231 | | |||
140 | KAUTH_HELPER_MAIN("org.kde.kio.file", FileHelper) | 232 | KAUTH_HELPER_MAIN("org.kde.kio.file", FileHelper) | ||
This early return skips all the deintialization code in the end of the function. Shouldn't it just be reply.setError(errno); like for all the other operations? maltek: This early return skips all the deintialization code in the end of the function. Shouldn't it… |
This is still subject to race conditions. You're opening the file with O_NOFOLLOW and calling fstat on it, correctly. But then, you don't return this file descriptor to further work on it. So there's no way to know if the file checked here and the file that's changed later on are the same.
(It's also leaking the opened file descriptor, currently.)