Import Debian changes 4.92-8+deb10u6
[hcoop/debian/exim4.git] / debian / patches / 84_23-CVE-2020-28007-Link-attack-in-Exim-s-log-directory.patch
1 From 99ae249e9857e80ff4d65b2388bc68c624dcb739 Mon Sep 17 00:00:00 2001
2 From: Qualys Security Advisory <qsa@qualys.com>
3 Date: Tue, 23 Feb 2021 08:33:03 -0800
4 Subject: [PATCH 23/29] CVE-2020-28007: Link attack in Exim's log directory
5
6 We patch this vulnerability by opening (instead of just creating) the
7 log file in an unprivileged (exim) child process, and by passing this
8 file descriptor back to the privileged (root) parent process. The two
9 functions log_send_fd() and log_recv_fd() are inspired by OpenSSH's
10 functions mm_send_fd() and mm_receive_fd(); thanks!
11
12 This patch also fixes:
13
14 - a NULL-pointer dereference in usr1_handler() (this signal handler is
15 installed before process_log_path is initialized);
16
17 - a file-descriptor leak in dmarc_write_history_file() (two return paths
18 did not close history_file_fd).
19
20 Note: the use of log_open_as_exim() in dmarc_write_history_file() should
21 be fine because the documentation explicitly states "Make sure the
22 directory of this file is writable by the user exim runs as."
23
24 (cherry picked from commit 2502cc41d1d92c1413eca6a4ba035c21162662bd)
25 (cherry picked from commit 93e9a18fbf09deb59bd133986f4c89aeb2d2d86a)
26 ---
27 src/dmarc.c | 179 ++++++++++++++++++------------------
28 src/exim.c | 14 +--
29 src/functions.h | 3 +-
30 src/log.c | 214 ++++++++++++++++++++++++++++----------------
31 test/stderr/0397 | 6 +-
32 5 files changed, 234 insertions(+), 182 deletions(-)
33
34 diff --git a/src/dmarc.c b/src/dmarc.c
35 index f29f7eba6..c5e01c7ee 100644
36 --- a/src/dmarc.c
37 +++ b/src/dmarc.c
38 @@ -204,6 +204,97 @@ if ( dmarc_policy == DMARC_POLICY_REJECT && action == DMARC_RESULT_REJECT
39 }
40 }
41
42 +
43 +static int
44 +dmarc_write_history_file()
45 +{
46 +int tmp_ans;
47 +u_char **rua; /* aggregate report addressees */
48 +uschar *history_buffer = NULL;
49 +
50 +if (!dmarc_history_file)
51 + {
52 + DEBUG(D_receive) debug_printf("DMARC history file not set\n");
53 + return DMARC_HIST_DISABLED;
54 + }
55 +
56 +/* Generate the contents of the history file */
57 +history_buffer = string_sprintf(
58 + "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n",
59 + message_id, primary_hostname, time(NULL), sender_host_address,
60 + header_from_sender, expand_string(US"$sender_address_domain"));
61 +
62 +if (spf_response)
63 + history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_ares_result);
64 + /* history_buffer = string_sprintf("%sspf -1\n", history_buffer); */
65 +
66 +history_buffer = string_sprintf(
67 + "%s%spdomain %s\npolicy %d\n",
68 + history_buffer, dkim_history_buffer, dmarc_used_domain, dmarc_policy);
69 +
70 +if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1)))
71 + for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++)
72 + history_buffer = string_sprintf("%srua %s\n", history_buffer, rua[tmp_ans]);
73 +else
74 + history_buffer = string_sprintf("%srua -\n", history_buffer);
75 +
76 +opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans);
77 +history_buffer = string_sprintf("%spct %d\n", history_buffer, tmp_ans);
78 +
79 +opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans);
80 +history_buffer = string_sprintf("%sadkim %d\n", history_buffer, tmp_ans);
81 +
82 +opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans);
83 +history_buffer = string_sprintf("%saspf %d\n", history_buffer, tmp_ans);
84 +
85 +opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
86 +history_buffer = string_sprintf("%sp %d\n", history_buffer, tmp_ans);
87 +
88 +opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans);
89 +history_buffer = string_sprintf("%ssp %d\n", history_buffer, tmp_ans);
90 +
91 +history_buffer = string_sprintf(
92 + "%salign_dkim %d\nalign_spf %d\naction %d\n",
93 + history_buffer, da, sa, action);
94 +
95 +/* Write the contents to the history file */
96 +DEBUG(D_receive)
97 + debug_printf("DMARC logging history data for opendmarc reporting%s\n",
98 + (host_checking || f.running_in_test_harness) ? " (not really)" : "");
99 +if (host_checking || f.running_in_test_harness)
100 + {
101 + DEBUG(D_receive)
102 + debug_printf("DMARC history data for debugging:\n%s", history_buffer);
103 + }
104 +else
105 + {
106 + ssize_t written_len;
107 + const int history_file_fd = log_open_as_exim(dmarc_history_file);
108 +
109 + if (history_file_fd < 0)
110 + {
111 + log_write(0, LOG_MAIN|LOG_PANIC, "failure to create DMARC history file: %s",
112 + dmarc_history_file);
113 + return DMARC_HIST_FILE_ERR;
114 + }
115 +
116 + written_len = write_to_fd_buf(history_file_fd,
117 + history_buffer,
118 + Ustrlen(history_buffer));
119 +
120 + (void)close(history_file_fd);
121 +
122 + if (written_len <= 0)
123 + {
124 + log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s",
125 + dmarc_history_file);
126 + return DMARC_HIST_WRITE_ERR;
127 + }
128 + }
129 +return DMARC_HIST_OK;
130 +}
131 +
132 +
133 /* dmarc_process adds the envelope sender address to the existing
134 context (if any), retrieves the result, sets up expansion
135 strings and evaluates the condition outcome. */
136 @@ -486,94 +577,6 @@ if (!f.dmarc_disable_verify)
137 return OK;
138 }
139
140 -static int
141 -dmarc_write_history_file()
142 -{
143 -int history_file_fd;
144 -ssize_t written_len;
145 -int tmp_ans;
146 -u_char **rua; /* aggregate report addressees */
147 -uschar *history_buffer = NULL;
148 -
149 -if (!dmarc_history_file)
150 - {
151 - DEBUG(D_receive) debug_printf("DMARC history file not set\n");
152 - return DMARC_HIST_DISABLED;
153 - }
154 -history_file_fd = log_create(dmarc_history_file);
155 -
156 -if (history_file_fd < 0)
157 - {
158 - log_write(0, LOG_MAIN|LOG_PANIC, "failure to create DMARC history file: %s",
159 - dmarc_history_file);
160 - return DMARC_HIST_FILE_ERR;
161 - }
162 -
163 -/* Generate the contents of the history file */
164 -history_buffer = string_sprintf(
165 - "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n",
166 - message_id, primary_hostname, time(NULL), sender_host_address,
167 - header_from_sender, expand_string(US"$sender_address_domain"));
168 -
169 -if (spf_response)
170 - history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_ares_result);
171 - /* history_buffer = string_sprintf("%sspf -1\n", history_buffer); */
172 -
173 -history_buffer = string_sprintf(
174 - "%s%spdomain %s\npolicy %d\n",
175 - history_buffer, dkim_history_buffer, dmarc_used_domain, dmarc_policy);
176 -
177 -if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1)))
178 - for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++)
179 - history_buffer = string_sprintf("%srua %s\n", history_buffer, rua[tmp_ans]);
180 -else
181 - history_buffer = string_sprintf("%srua -\n", history_buffer);
182 -
183 -opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans);
184 -history_buffer = string_sprintf("%spct %d\n", history_buffer, tmp_ans);
185 -
186 -opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans);
187 -history_buffer = string_sprintf("%sadkim %d\n", history_buffer, tmp_ans);
188 -
189 -opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans);
190 -history_buffer = string_sprintf("%saspf %d\n", history_buffer, tmp_ans);
191 -
192 -opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
193 -history_buffer = string_sprintf("%sp %d\n", history_buffer, tmp_ans);
194 -
195 -opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans);
196 -history_buffer = string_sprintf("%ssp %d\n", history_buffer, tmp_ans);
197 -
198 -history_buffer = string_sprintf(
199 - "%salign_dkim %d\nalign_spf %d\naction %d\n",
200 - history_buffer, da, sa, action);
201 -
202 -/* Write the contents to the history file */
203 -DEBUG(D_receive)
204 - debug_printf("DMARC logging history data for opendmarc reporting%s\n",
205 - (host_checking || f.running_in_test_harness) ? " (not really)" : "");
206 -if (host_checking || f.running_in_test_harness)
207 - {
208 - DEBUG(D_receive)
209 - debug_printf("DMARC history data for debugging:\n%s", history_buffer);
210 - }
211 -else
212 - {
213 - written_len = write_to_fd_buf(history_file_fd,
214 - history_buffer,
215 - Ustrlen(history_buffer));
216 - if (written_len == 0)
217 - {
218 - log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s",
219 - dmarc_history_file);
220 - return DMARC_HIST_WRITE_ERR;
221 - }
222 - (void)close(history_file_fd);
223 - }
224 -return DMARC_HIST_OK;
225 -}
226 -
227 -
228 uschar *
229 dmarc_exim_expand_query(int what)
230 {
231 diff --git a/src/exim.c b/src/exim.c
232 index a7dc48c4e..f0a168983 100644
233 --- a/src/exim.c
234 +++ b/src/exim.c
235 @@ -227,18 +227,8 @@ int fd;
236
237 os_restarting_signal(sig, usr1_handler);
238
239 -if ((fd = Uopen(process_log_path, O_APPEND|O_WRONLY, LOG_MODE)) < 0)
240 - {
241 - /* If we are already running as the Exim user, try to create it in the
242 - current process (assuming spool_directory exists). Otherwise, if we are
243 - root, do the creation in an exim:exim subprocess. */
244 -
245 - int euid = geteuid();
246 - if (euid == exim_uid)
247 - fd = Uopen(process_log_path, O_CREAT|O_APPEND|O_WRONLY, LOG_MODE);
248 - else if (euid == root_uid)
249 - fd = log_create_as_exim(process_log_path);
250 - }
251 +if (!process_log_path) return;
252 +fd = log_open_as_exim(process_log_path);
253
254 /* If we are neither exim nor root, or if we failed to create the log file,
255 give up. There is not much useful we can do with errors, since we don't want
256 diff --git a/src/functions.h b/src/functions.h
257 index cab7a7363..366cb2f26 100644
258 --- a/src/functions.h
259 +++ b/src/functions.h
260 @@ -281,8 +281,7 @@ extern int ip_streamsocket(const uschar *, uschar **, int);
261 extern int ipv6_nmtoa(int *, uschar *);
262
263 extern uschar *local_part_quote(uschar *);
264 -extern int log_create(uschar *);
265 -extern int log_create_as_exim(uschar *);
266 +extern int log_open_as_exim(uschar *);
267 extern void log_close_all(void);
268
269 extern macro_item * macro_create(const uschar *, const uschar *, BOOL);
270 diff --git a/src/log.c b/src/log.c
271 index c8313890e..15c88c13e 100644
272 --- a/src/log.c
273 +++ b/src/log.c
274 @@ -264,14 +264,19 @@ overwrite it temporarily if it is necessary to create the directory.
275 Returns: a file descriptor, or < 0 on failure (errno set)
276 */
277
278 -int
279 -log_create(uschar *name)
280 +static int
281 +log_open_already_exim(uschar * const name)
282 {
283 -int fd = Uopen(name,
284 -#ifdef O_CLOEXEC
285 - O_CLOEXEC |
286 -#endif
287 - O_CREAT|O_APPEND|O_WRONLY, LOG_MODE);
288 +int fd = -1;
289 +const int flags = O_WRONLY | O_APPEND | O_CREAT | O_NONBLOCK;
290 +
291 +if (geteuid() != exim_uid)
292 + {
293 + errno = EACCES;
294 + return -1;
295 + }
296 +
297 +fd = Uopen(name, flags, LOG_MODE);
298
299 /* If creation failed, attempt to build a log directory in case that is the
300 problem. */
301 @@ -285,11 +290,7 @@ if (fd < 0 && errno == ENOENT)
302 DEBUG(D_any) debug_printf("%s log directory %s\n",
303 created ? "created" : "failed to create", name);
304 *lastslash = '/';
305 - if (created) fd = Uopen(name,
306 -#ifdef O_CLOEXEC
307 - O_CLOEXEC |
308 -#endif
309 - O_CREAT|O_APPEND|O_WRONLY, LOG_MODE);
310 + if (created) fd = Uopen(name, flags, LOG_MODE);
311 }
312
313 return fd;
314 @@ -297,6 +298,81 @@ return fd;
315
316
317
318 +/* Inspired by OpenSSH's mm_send_fd(). Thanks! */
319 +
320 +static int
321 +log_send_fd(const int sock, const int fd)
322 +{
323 +struct msghdr msg;
324 +union {
325 + struct cmsghdr hdr;
326 + char buf[CMSG_SPACE(sizeof(int))];
327 +} cmsgbuf;
328 +struct cmsghdr *cmsg;
329 +struct iovec vec;
330 +char ch = 'A';
331 +ssize_t n;
332 +
333 +memset(&msg, 0, sizeof(msg));
334 +memset(&cmsgbuf, 0, sizeof(cmsgbuf));
335 +msg.msg_control = &cmsgbuf.buf;
336 +msg.msg_controllen = sizeof(cmsgbuf.buf);
337 +
338 +cmsg = CMSG_FIRSTHDR(&msg);
339 +cmsg->cmsg_len = CMSG_LEN(sizeof(int));
340 +cmsg->cmsg_level = SOL_SOCKET;
341 +cmsg->cmsg_type = SCM_RIGHTS;
342 +*(int *)CMSG_DATA(cmsg) = fd;
343 +
344 +vec.iov_base = &ch;
345 +vec.iov_len = 1;
346 +msg.msg_iov = &vec;
347 +msg.msg_iovlen = 1;
348 +
349 +while ((n = sendmsg(sock, &msg, 0)) == -1 && errno == EINTR);
350 +if (n != 1) return -1;
351 +return 0;
352 +}
353 +
354 +/* Inspired by OpenSSH's mm_receive_fd(). Thanks! */
355 +
356 +static int
357 +log_recv_fd(const int sock)
358 +{
359 +struct msghdr msg;
360 +union {
361 + struct cmsghdr hdr;
362 + char buf[CMSG_SPACE(sizeof(int))];
363 +} cmsgbuf;
364 +struct cmsghdr *cmsg;
365 +struct iovec vec;
366 +ssize_t n;
367 +char ch = '\0';
368 +int fd = -1;
369 +
370 +memset(&msg, 0, sizeof(msg));
371 +vec.iov_base = &ch;
372 +vec.iov_len = 1;
373 +msg.msg_iov = &vec;
374 +msg.msg_iovlen = 1;
375 +
376 +memset(&cmsgbuf, 0, sizeof(cmsgbuf));
377 +msg.msg_control = &cmsgbuf.buf;
378 +msg.msg_controllen = sizeof(cmsgbuf.buf);
379 +
380 +while ((n = recvmsg(sock, &msg, 0)) == -1 && errno == EINTR);
381 +if (n != 1 || ch != 'A') return -1;
382 +
383 +cmsg = CMSG_FIRSTHDR(&msg);
384 +if (cmsg == NULL) return -1;
385 +if (cmsg->cmsg_type != SCM_RIGHTS) return -1;
386 +fd = *(const int *)CMSG_DATA(cmsg);
387 +if (fd < 0) return -1;
388 +return fd;
389 +}
390 +
391 +
392 +
393 /*************************************************
394 * Create a log file as the exim user *
395 *************************************************/
396 @@ -312,41 +388,60 @@ Returns: a file descriptor, or < 0 on failure (errno set)
397 */
398
399 int
400 -log_create_as_exim(uschar *name)
401 +log_open_as_exim(uschar * const name)
402 {
403 -pid_t pid = fork();
404 -int status = 1;
405 int fd = -1;
406 +const uid_t euid = geteuid();
407
408 -/* In the subprocess, change uid/gid and do the creation. Return 0 from the
409 -subprocess on success. If we don't check for setuid failures, then the file
410 -can be created as root, so vulnerabilities which cause setuid to fail mean
411 -that the Exim user can use symlinks to cause a file to be opened/created as
412 -root. We always open for append, so can't nuke existing content but it would
413 -still be Rather Bad. */
414 -
415 -if (pid == 0)
416 +if (euid == exim_uid)
417 {
418 - if (setgid(exim_gid) < 0)
419 - die(US"exim: setgid for log-file creation failed, aborting",
420 - US"Unexpected log failure, please try later");
421 - if (setuid(exim_uid) < 0)
422 - die(US"exim: setuid for log-file creation failed, aborting",
423 - US"Unexpected log failure, please try later");
424 - _exit((log_create(name) < 0)? 1 : 0);
425 + fd = log_open_already_exim(name);
426 }
427 +else if (euid == root_uid)
428 + {
429 + int sock[2];
430 + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock) == 0)
431 + {
432 + const pid_t pid = fork();
433 + if (pid == 0)
434 + {
435 + (void)close(sock[0]);
436 + if (setgroups(1, &exim_gid) != 0) _exit(EXIT_FAILURE);
437 + if (setgid(exim_gid) != 0) _exit(EXIT_FAILURE);
438 + if (setuid(exim_uid) != 0) _exit(EXIT_FAILURE);
439 +
440 + if (getuid() != exim_uid || geteuid() != exim_uid) _exit(EXIT_FAILURE);
441 + if (getgid() != exim_gid || getegid() != exim_gid) _exit(EXIT_FAILURE);
442 +
443 + fd = log_open_already_exim(name);
444 + if (fd < 0) _exit(EXIT_FAILURE);
445 + if (log_send_fd(sock[1], fd) != 0) _exit(EXIT_FAILURE);
446 + (void)close(sock[1]);
447 + _exit(EXIT_SUCCESS);
448 + }
449
450 -/* If we created a subprocess, wait for it. If it succeeded, try the open. */
451 -
452 -while (pid > 0 && waitpid(pid, &status, 0) != pid);
453 -if (status == 0) fd = Uopen(name,
454 -#ifdef O_CLOEXEC
455 - O_CLOEXEC |
456 -#endif
457 - O_APPEND|O_WRONLY, LOG_MODE);
458 + (void)close(sock[1]);
459 + if (pid > 0)
460 + {
461 + fd = log_recv_fd(sock[0]);
462 + while (waitpid(pid, NULL, 0) == -1 && errno == EINTR);
463 + }
464 + (void)close(sock[0]);
465 + }
466 + }
467
468 -/* If we failed to create a subprocess, we are in a bad way. We return
469 -with fd still < 0, and errno set, letting the caller handle the error. */
470 +if (fd >= 0)
471 + {
472 + int flags;
473 + flags = fcntl(fd, F_GETFD);
474 + if (flags != -1) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
475 + flags = fcntl(fd, F_GETFL);
476 + if (flags != -1) (void)fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
477 + }
478 +else
479 + {
480 + errno = EACCES;
481 + }
482
483 return fd;
484 }
485 @@ -459,52 +554,17 @@ if (!ok)
486 die(US"exim: log file path too long: aborting",
487 US"Logging failure; please try later");
488
489 -/* We now have the file name. Try to open an existing file. After a successful
490 -open, arrange for automatic closure on exec(), and then return. */
491 +/* We now have the file name. After a successful open, return. */
492
493 -*fd = Uopen(buffer,
494 -#ifdef O_CLOEXEC
495 - O_CLOEXEC |
496 -#endif
497 - O_APPEND|O_WRONLY, LOG_MODE);
498 +*fd = log_open_as_exim(buffer);
499
500 if (*fd >= 0)
501 {
502 -#ifndef O_CLOEXEC
503 - (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC);
504 -#endif
505 return;
506 }
507
508 -/* Open was not successful: try creating the file. If this is a root process,
509 -we must do the creating in a subprocess set to exim:exim in order to ensure
510 -that the file is created with the right ownership. Otherwise, there can be a
511 -race if another Exim process is trying to write to the log at the same time.
512 -The use of SIGUSR1 by the exiwhat utility can provoke a lot of simultaneous
513 -writing. */
514 -
515 euid = geteuid();
516
517 -/* If we are already running as the Exim user (even if that user is root),
518 -we can go ahead and create in the current process. */
519 -
520 -if (euid == exim_uid) *fd = log_create(buffer);
521 -
522 -/* Otherwise, if we are root, do the creation in an exim:exim subprocess. If we
523 -are neither exim nor root, creation is not attempted. */
524 -
525 -else if (euid == root_uid) *fd = log_create_as_exim(buffer);
526 -
527 -/* If we now have an open file, set the close-on-exec flag and return. */
528 -
529 -if (*fd >= 0)
530 - {
531 -#ifndef O_CLOEXEC
532 - (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC);
533 -#endif
534 - return;
535 - }
536 -
537 /* Creation failed. There are some circumstances in which we get here when
538 the effective uid is not root or exim, which is the problem. (For example, a
539 non-setuid binary with log_arguments set, called in certain ways.) Rather than
540 --
541 2.30.2
542