diff options
| author | Paul Eggert | 2021-07-23 13:33:21 +0200 |
|---|---|---|
| committer | Lars Ingebrigtsen | 2021-07-23 13:33:37 +0200 |
| commit | 007744dd0404d6febca88b00c22981cc630fb8c0 (patch) | |
| tree | 34e0756665429089a8098bc091eaf03a2032a8db /lib-src | |
| parent | 55a19a1da26d35673c8eb2c52171ff3b31594dd9 (diff) | |
| download | emacs-007744dd0404d6febca88b00c22981cc630fb8c0.tar.gz emacs-007744dd0404d6febca88b00c22981cc630fb8c0.zip | |
Redo emacsclient socket symlink-attack checking
* admin/merge-gnulib (GNULIB_MODULES): Add file-has-acl.
* lib/file-has-acl.c: New file, copied from Gnulib.
* lib/gnulib.mk.in, m4/gnulib-comp.m4: Regenerate.
* lib-src/emacsclient.c: Include acl.h, for file_has_acl.
(O_PATH): Default to O_SEARCH, which is good enough here.
(union local_sockaddr): New type.
(socket_status): Remove, replacing with ...
(connect_socket): New function. All callers changed.
This function checks for ownership and permissions issues with the
parent directory of the socket file, instead of checking the
owner of the socket (which does not help security).
(socknamesize): Move to file scope.
(local_sockname): New arg S. No need to pass socknamesize.
UID arg is now uid_t. All callers changed. Get file descriptor
of parent directory of socket, to foil some symlink attacks.
Do not follow symlinks to that directory.
(set_local_socket): Create the socket here instead of on
each attempt to connect it. Fall back from XDG_RUNTIME_DIR
to /tmp only if the former fails due to ENOENT. Adjust
permission-failure diagnostic to match changed behavior.
This addresses Bug#33847, which complained about emacsclient in a
safer XDG environment not connecting to an Emacs server running in
a less-safe enviroment outside XDG. The patch fixes a
longstanding issue with emacsclient permission checking.
It’s ineffective to look at the permission of the socket file
itself; on some platforms, these permissions are ignored anyway.
What matters are the permissions on the parent directory of the
socket file, as these are what make symlink attacks possible.
Change the permissions check accordingly, and also refuse to
follow symlinks to that parent directory. These changes make it
OK for emacsclient to fall back from XDG_RUNTIME_DIR to the
traditionally less-safe /tmp/emacsNNNN directories, since /tmp is
universally sticky nowadays.
Diffstat (limited to 'lib-src')
| -rw-r--r-- | lib-src/emacsclient.c | 226 |
1 files changed, 147 insertions, 79 deletions
diff --git a/lib-src/emacsclient.c b/lib-src/emacsclient.c index 12ced4aadbd..8346524a3eb 100644 --- a/lib-src/emacsclient.c +++ b/lib-src/emacsclient.c | |||
| @@ -80,6 +80,9 @@ char *w32_getenv (const char *); | |||
| 80 | #include <sys/stat.h> | 80 | #include <sys/stat.h> |
| 81 | #include <unistd.h> | 81 | #include <unistd.h> |
| 82 | 82 | ||
| 83 | #ifndef WINDOWSNT | ||
| 84 | # include <acl.h> | ||
| 85 | #endif | ||
| 83 | #include <filename.h> | 86 | #include <filename.h> |
| 84 | #include <intprops.h> | 87 | #include <intprops.h> |
| 85 | #include <min-max.h> | 88 | #include <min-max.h> |
| @@ -91,6 +94,10 @@ char *w32_getenv (const char *); | |||
| 91 | # pragma GCC diagnostic ignored "-Wformat-truncation=2" | 94 | # pragma GCC diagnostic ignored "-Wformat-truncation=2" |
| 92 | #endif | 95 | #endif |
| 93 | 96 | ||
| 97 | #if !defined O_PATH && !defined WINDOWSNT | ||
| 98 | # define O_PATH O_SEARCH | ||
| 99 | #endif | ||
| 100 | |||
| 94 | 101 | ||
| 95 | /* Name used to invoke this program. */ | 102 | /* Name used to invoke this program. */ |
| 96 | static char const *progname; | 103 | static char const *progname; |
| @@ -1128,24 +1135,74 @@ process_grouping (void) | |||
| 1128 | 1135 | ||
| 1129 | #ifdef SOCKETS_IN_FILE_SYSTEM | 1136 | #ifdef SOCKETS_IN_FILE_SYSTEM |
| 1130 | 1137 | ||
| 1131 | /* Return the file status of NAME, ordinarily a socket. | 1138 | /* A local socket address. The union avoids the need to cast. */ |
| 1132 | It should be owned by UID. Return one of the following: | 1139 | union local_sockaddr |
| 1133 | >0 - 'stat' failed with this errno value | 1140 | { |
| 1134 | -1 - isn't owned by us | 1141 | struct sockaddr_un un; |
| 1135 | 0 - success: none of the above */ | 1142 | struct sockaddr sa; |
| 1143 | }; | ||
| 1144 | |||
| 1145 | /* Relative to the directory DIRFD, connect the socket file named ADDR | ||
| 1146 | to the socket S. Return 0 if successful, -1 if DIRFD is not | ||
| 1147 | AT_FDCWD and DIRFD's permissions would allow a symlink attack, an | ||
| 1148 | errno otherwise. */ | ||
| 1136 | 1149 | ||
| 1137 | static int | 1150 | static int |
| 1138 | socket_status (const char *name, uid_t uid) | 1151 | connect_socket (int dirfd, char const *addr, int s, uid_t uid) |
| 1139 | { | 1152 | { |
| 1140 | struct stat statbfr; | 1153 | int sock_status = 0; |
| 1141 | 1154 | ||
| 1142 | if (stat (name, &statbfr) != 0) | 1155 | union local_sockaddr server; |
| 1143 | return errno; | 1156 | if (sizeof server.un.sun_path <= strlen (addr)) |
| 1157 | return ENAMETOOLONG; | ||
| 1158 | server.un.sun_family = AF_UNIX; | ||
| 1159 | strcpy (server.un.sun_path, addr); | ||
| 1144 | 1160 | ||
| 1145 | if (statbfr.st_uid != uid) | 1161 | /* If -1, WDFD is not set yet. If nonnegative, WDFD is a file |
| 1146 | return -1; | 1162 | descriptor for the initial working directory. Otherwise -1 - WDFD is |
| 1163 | the error number for the initial working directory. */ | ||
| 1164 | static int wdfd = -1; | ||
| 1147 | 1165 | ||
| 1148 | return 0; | 1166 | if (dirfd != AT_FDCWD) |
| 1167 | { | ||
| 1168 | /* Fail if DIRFD's permissions are bogus. */ | ||
| 1169 | struct stat st; | ||
| 1170 | if (fstat (dirfd, &st) != 0) | ||
| 1171 | return errno; | ||
| 1172 | if (st.st_uid != uid || (st.st_mode & (S_IWGRP | S_IWOTH))) | ||
| 1173 | return -1; | ||
| 1174 | |||
| 1175 | if (wdfd == -1) | ||
| 1176 | { | ||
| 1177 | /* Save the initial working directory. */ | ||
| 1178 | wdfd = open (".", O_PATH | O_CLOEXEC); | ||
| 1179 | if (wdfd < 0) | ||
| 1180 | wdfd = -1 - errno; | ||
| 1181 | } | ||
| 1182 | if (wdfd < 0) | ||
| 1183 | return -1 - wdfd; | ||
| 1184 | if (fchdir (dirfd) != 0) | ||
| 1185 | return errno; | ||
| 1186 | |||
| 1187 | /* Fail if DIRFD has an ACL, which means its permissions are | ||
| 1188 | almost surely bogus. */ | ||
| 1189 | int has_acl = file_has_acl (".", &st); | ||
| 1190 | if (has_acl) | ||
| 1191 | sock_status = has_acl < 0 ? errno : -1; | ||
| 1192 | } | ||
| 1193 | |||
| 1194 | if (!sock_status) | ||
| 1195 | sock_status = connect (s, &server.sa, sizeof server.un) == 0 ? 0 : errno; | ||
| 1196 | |||
| 1197 | /* Fail immediately if we cannot change back to the initial working | ||
| 1198 | directory, as that can mess up the rest of execution. */ | ||
| 1199 | if (dirfd != AT_FDCWD && fchdir (wdfd) != 0) | ||
| 1200 | { | ||
| 1201 | message (true, "%s: .: %s\n", progname, strerror (errno)); | ||
| 1202 | exit (EXIT_FAILURE); | ||
| 1203 | } | ||
| 1204 | |||
| 1205 | return sock_status; | ||
| 1149 | } | 1206 | } |
| 1150 | 1207 | ||
| 1151 | 1208 | ||
| @@ -1322,32 +1379,49 @@ act_on_signals (HSOCKET emacs_socket) | |||
| 1322 | } | 1379 | } |
| 1323 | } | 1380 | } |
| 1324 | 1381 | ||
| 1325 | /* Create in SOCKNAME (of size SOCKNAMESIZE) a name for a local socket. | 1382 | enum { socknamesize = sizeof ((struct sockaddr_un *) NULL)->sun_path }; |
| 1326 | The first TMPDIRLEN bytes of SOCKNAME are already initialized to be | 1383 | |
| 1327 | the name of a temporary directory. Use UID and SERVER_NAME to | 1384 | /* Given a local socket S, create in *SOCKNAME a name for a local socket |
| 1328 | concoct the name. Return the total length of the name if successful, | 1385 | and connect to that socket. The first TMPDIRLEN bytes of *SOCKNAME are |
| 1329 | -1 if it does not fit (and store a truncated name in that case). | 1386 | already initialized to be the name of a temporary directory. |
| 1330 | Fail if TMPDIRLEN is out of range. */ | 1387 | Use UID and SERVER_NAME to concoct the name. Return 0 if |
| 1388 | successful, -1 if the socket's parent directory is not safe, and an | ||
| 1389 | errno if there is some other problem. */ | ||
| 1331 | 1390 | ||
| 1332 | static int | 1391 | static int |
| 1333 | local_sockname (char *sockname, int socknamesize, int tmpdirlen, | 1392 | local_sockname (int s, char sockname[socknamesize], int tmpdirlen, |
| 1334 | uintmax_t uid, char const *server_name) | 1393 | uid_t uid, char const *server_name) |
| 1335 | { | 1394 | { |
| 1336 | /* If ! (0 <= TMPDIRLEN && TMPDIRLEN < SOCKNAMESIZE) the truncated | 1395 | /* If ! (0 <= TMPDIRLEN && TMPDIRLEN < SOCKNAMESIZE) the truncated |
| 1337 | temporary directory name is already in SOCKNAME, so nothing more | 1396 | temporary directory name is already in SOCKNAME, so nothing more |
| 1338 | need be stored. */ | 1397 | need be stored. */ |
| 1339 | if (0 <= tmpdirlen) | 1398 | if (! (0 <= tmpdirlen && tmpdirlen < socknamesize)) |
| 1340 | { | 1399 | return ENAMETOOLONG; |
| 1341 | int remaining = socknamesize - tmpdirlen; | 1400 | |
| 1342 | if (0 < remaining) | 1401 | /* Put the full address name into the buffer, since the caller might |
| 1343 | { | 1402 | need it for diagnostics. But don't overrun the buffer. */ |
| 1344 | int suffixlen = snprintf (&sockname[tmpdirlen], remaining, | 1403 | uintmax_t uidmax = uid; |
| 1345 | "/emacs%"PRIuMAX"/%s", uid, server_name); | 1404 | int emacsdirlen; |
| 1346 | if (0 <= suffixlen && suffixlen < remaining) | 1405 | int suffixlen = snprintf (sockname + tmpdirlen, socknamesize - tmpdirlen, |
| 1347 | return tmpdirlen + suffixlen; | 1406 | "/emacs%"PRIuMAX"%n/%s", uidmax, &emacsdirlen, |
| 1348 | } | 1407 | server_name); |
| 1349 | } | 1408 | if (! (0 <= suffixlen && suffixlen < socknamesize - tmpdirlen)) |
| 1350 | return -1; | 1409 | return ENAMETOOLONG; |
| 1410 | |||
| 1411 | /* Make sure the address's parent directory is not a symlink and is | ||
| 1412 | this user's directory and does not let others write to it; this | ||
| 1413 | fends off some symlink attacks. To avoid races, keep the parent | ||
| 1414 | directory open while checking. */ | ||
| 1415 | char *emacsdirend = sockname + tmpdirlen + emacsdirlen; | ||
| 1416 | *emacsdirend = '\0'; | ||
| 1417 | int dir = openat (AT_FDCWD, sockname, | ||
| 1418 | O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); | ||
| 1419 | *emacsdirend = '/'; | ||
| 1420 | if (dir < 0) | ||
| 1421 | return errno; | ||
| 1422 | int sock_status = connect_socket (dir, server_name, s, uid); | ||
| 1423 | close (dir); | ||
| 1424 | return sock_status; | ||
| 1351 | } | 1425 | } |
| 1352 | 1426 | ||
| 1353 | /* Create a local socket for SERVER_NAME and connect it to Emacs. If | 1427 | /* Create a local socket for SERVER_NAME and connect it to Emacs. If |
| @@ -1358,28 +1432,43 @@ local_sockname (char *sockname, int socknamesize, int tmpdirlen, | |||
| 1358 | static HSOCKET | 1432 | static HSOCKET |
| 1359 | set_local_socket (char const *server_name) | 1433 | set_local_socket (char const *server_name) |
| 1360 | { | 1434 | { |
| 1361 | union { | 1435 | union local_sockaddr server; |
| 1362 | struct sockaddr_un un; | 1436 | int sock_status; |
| 1363 | struct sockaddr sa; | ||
| 1364 | } server = {{ .sun_family = AF_UNIX }}; | ||
| 1365 | char *sockname = server.un.sun_path; | 1437 | char *sockname = server.un.sun_path; |
| 1366 | enum { socknamesize = sizeof server.un.sun_path }; | ||
| 1367 | int tmpdirlen = -1; | 1438 | int tmpdirlen = -1; |
| 1368 | int socknamelen = -1; | 1439 | int socknamelen = -1; |
| 1369 | uid_t uid = geteuid (); | 1440 | uid_t uid = geteuid (); |
| 1370 | bool tmpdir_used = false; | 1441 | bool tmpdir_used = false; |
| 1442 | int s = cloexec_socket (AF_UNIX, SOCK_STREAM, 0); | ||
| 1443 | if (s < 0) | ||
| 1444 | { | ||
| 1445 | message (true, "%s: can't create socket: %s\n", | ||
| 1446 | progname, strerror (errno)); | ||
| 1447 | fail (); | ||
| 1448 | } | ||
| 1371 | 1449 | ||
| 1372 | if (strchr (server_name, '/') | 1450 | if (strchr (server_name, '/') |
| 1373 | || (ISSLASH ('\\') && strchr (server_name, '\\'))) | 1451 | || (ISSLASH ('\\') && strchr (server_name, '\\'))) |
| 1374 | socknamelen = snprintf (sockname, socknamesize, "%s", server_name); | 1452 | { |
| 1453 | socknamelen = snprintf (sockname, socknamesize, "%s", server_name); | ||
| 1454 | sock_status = (0 <= socknamelen && socknamelen < socknamesize | ||
| 1455 | ? connect_socket (AT_FDCWD, sockname, s, 0) | ||
| 1456 | : ENAMETOOLONG); | ||
| 1457 | } | ||
| 1375 | else | 1458 | else |
| 1376 | { | 1459 | { |
| 1377 | /* socket_name is a file name component. */ | 1460 | /* socket_name is a file name component. */ |
| 1461 | sock_status = ENOENT; | ||
| 1378 | char const *xdg_runtime_dir = egetenv ("XDG_RUNTIME_DIR"); | 1462 | char const *xdg_runtime_dir = egetenv ("XDG_RUNTIME_DIR"); |
| 1379 | if (xdg_runtime_dir) | 1463 | if (xdg_runtime_dir) |
| 1380 | socknamelen = snprintf (sockname, socknamesize, "%s/emacs/%s", | 1464 | { |
| 1381 | xdg_runtime_dir, server_name); | 1465 | socknamelen = snprintf (sockname, socknamesize, "%s/emacs/%s", |
| 1382 | else | 1466 | xdg_runtime_dir, server_name); |
| 1467 | sock_status = (0 <= socknamelen && socknamelen < socknamesize | ||
| 1468 | ? connect_socket (AT_FDCWD, sockname, s, 0) | ||
| 1469 | : ENAMETOOLONG); | ||
| 1470 | } | ||
| 1471 | if (sock_status == ENOENT) | ||
| 1383 | { | 1472 | { |
| 1384 | char const *tmpdir = egetenv ("TMPDIR"); | 1473 | char const *tmpdir = egetenv ("TMPDIR"); |
| 1385 | if (tmpdir) | 1474 | if (tmpdir) |
| @@ -1398,23 +1487,24 @@ set_local_socket (char const *server_name) | |||
| 1398 | if (tmpdirlen < 0) | 1487 | if (tmpdirlen < 0) |
| 1399 | tmpdirlen = snprintf (sockname, socknamesize, "/tmp"); | 1488 | tmpdirlen = snprintf (sockname, socknamesize, "/tmp"); |
| 1400 | } | 1489 | } |
| 1401 | socknamelen = local_sockname (sockname, socknamesize, tmpdirlen, | 1490 | sock_status = local_sockname (s, sockname, tmpdirlen, |
| 1402 | uid, server_name); | 1491 | uid, server_name); |
| 1403 | tmpdir_used = true; | 1492 | tmpdir_used = true; |
| 1404 | } | 1493 | } |
| 1405 | } | 1494 | } |
| 1406 | 1495 | ||
| 1407 | if (! (0 <= socknamelen && socknamelen < socknamesize)) | 1496 | if (sock_status == 0) |
| 1497 | return s; | ||
| 1498 | |||
| 1499 | if (sock_status == ENAMETOOLONG) | ||
| 1408 | { | 1500 | { |
| 1409 | message (true, "%s: socket-name %s... too long\n", progname, sockname); | 1501 | message (true, "%s: socket-name %s... too long\n", progname, sockname); |
| 1410 | fail (); | 1502 | fail (); |
| 1411 | } | 1503 | } |
| 1412 | 1504 | ||
| 1413 | /* See if the socket exists, and if it's owned by us. */ | 1505 | if (tmpdir_used) |
| 1414 | int sock_status = socket_status (sockname, uid); | ||
| 1415 | if (sock_status) | ||
| 1416 | { | 1506 | { |
| 1417 | /* Failing that, see if LOGNAME or USER exist and differ from | 1507 | /* See whether LOGNAME or USER exist and differ from |
| 1418 | our euid. If so, look for a socket based on the UID | 1508 | our euid. If so, look for a socket based on the UID |
| 1419 | associated with the name. This is reminiscent of the logic | 1509 | associated with the name. This is reminiscent of the logic |
| 1420 | that init_editfns uses to set the global Vuser_full_name. */ | 1510 | that init_editfns uses to set the global Vuser_full_name. */ |
| @@ -1431,48 +1521,26 @@ set_local_socket (char const *server_name) | |||
| 1431 | if (pw && pw->pw_uid != uid) | 1521 | if (pw && pw->pw_uid != uid) |
| 1432 | { | 1522 | { |
| 1433 | /* We're running under su, apparently. */ | 1523 | /* We're running under su, apparently. */ |
| 1434 | socknamelen = local_sockname (sockname, socknamesize, tmpdirlen, | 1524 | sock_status = local_sockname (s, sockname, tmpdirlen, |
| 1435 | pw->pw_uid, server_name); | 1525 | pw->pw_uid, server_name); |
| 1436 | if (socknamelen < 0) | 1526 | if (sock_status == 0) |
| 1527 | return s; | ||
| 1528 | if (sock_status == ENAMETOOLONG) | ||
| 1437 | { | 1529 | { |
| 1438 | message (true, "%s: socket-name %s... too long\n", | 1530 | message (true, "%s: socket-name %s... too long\n", |
| 1439 | progname, sockname); | 1531 | progname, sockname); |
| 1440 | exit (EXIT_FAILURE); | 1532 | exit (EXIT_FAILURE); |
| 1441 | } | 1533 | } |
| 1442 | |||
| 1443 | sock_status = socket_status (sockname, uid); | ||
| 1444 | } | 1534 | } |
| 1445 | } | 1535 | } |
| 1446 | } | 1536 | } |
| 1447 | 1537 | ||
| 1448 | if (sock_status == 0) | 1538 | close (s); |
| 1449 | { | ||
| 1450 | HSOCKET s = cloexec_socket (AF_UNIX, SOCK_STREAM, 0); | ||
| 1451 | if (s < 0) | ||
| 1452 | { | ||
| 1453 | message (true, "%s: socket: %s\n", progname, strerror (errno)); | ||
| 1454 | return INVALID_SOCKET; | ||
| 1455 | } | ||
| 1456 | if (connect (s, &server.sa, sizeof server.un) != 0) | ||
| 1457 | { | ||
| 1458 | message (true, "%s: connect: %s\n", progname, strerror (errno)); | ||
| 1459 | CLOSE_SOCKET (s); | ||
| 1460 | return INVALID_SOCKET; | ||
| 1461 | } | ||
| 1462 | 1539 | ||
| 1463 | struct stat connect_stat; | 1540 | if (sock_status == -1) |
| 1464 | if (fstat (s, &connect_stat) != 0) | 1541 | message (true, |
| 1465 | sock_status = errno; | 1542 | "%s: Invalid permissions on parent directory of socket: %s\n", |
| 1466 | else if (connect_stat.st_uid == uid) | 1543 | progname, sockname); |
| 1467 | return s; | ||
| 1468 | else | ||
| 1469 | sock_status = -1; | ||
| 1470 | |||
| 1471 | CLOSE_SOCKET (s); | ||
| 1472 | } | ||
| 1473 | |||
| 1474 | if (sock_status < 0) | ||
| 1475 | message (true, "%s: Invalid socket owner\n", progname); | ||
| 1476 | else if (sock_status == ENOENT) | 1544 | else if (sock_status == ENOENT) |
| 1477 | { | 1545 | { |
| 1478 | if (tmpdir_used) | 1546 | if (tmpdir_used) |
| @@ -1502,7 +1570,7 @@ set_local_socket (char const *server_name) | |||
| 1502 | } | 1570 | } |
| 1503 | } | 1571 | } |
| 1504 | else | 1572 | else |
| 1505 | message (true, "%s: can't stat %s: %s\n", | 1573 | message (true, "%s: can't connect to %s: %s\n", |
| 1506 | progname, sockname, strerror (sock_status)); | 1574 | progname, sockname, strerror (sock_status)); |
| 1507 | 1575 | ||
| 1508 | return INVALID_SOCKET; | 1576 | return INVALID_SOCKET; |