diff options
author | Yuuki Harano <masm+github@masm11.me> | 2021-07-25 23:34:55 +0900 |
---|---|---|
committer | Yuuki Harano <masm+github@masm11.me> | 2021-07-25 23:34:55 +0900 |
commit | 13a9a5e836cbe6e64aadaba40fe1f7eb83320d08 (patch) | |
tree | 242ac1f485cf6762680a904952747d63b295e198 /lib-src | |
parent | b242394f24b154f8e20f5abf4b2f826629e99ea6 (diff) | |
parent | 41a55a330f518254da795719ac6e3085254d4110 (diff) | |
download | emacs-13a9a5e836cbe6e64aadaba40fe1f7eb83320d08.tar.gz emacs-13a9a5e836cbe6e64aadaba40fe1f7eb83320d08.tar.bz2 emacs-13a9a5e836cbe6e64aadaba40fe1f7eb83320d08.zip |
Merge branch 'master' of git.sv.gnu.org:/srv/git/emacs into feature/pgtk
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 6d3f827a253..8fa571fd4ef 100644 --- a/lib-src/emacsclient.c +++ b/lib-src/emacsclient.c @@ -80,6 +80,9 @@ char *w32_getenv (const char *); #include <sys/stat.h> #include <unistd.h> +#ifndef WINDOWSNT +# include <acl.h> +#endif #include <filename.h> #include <intprops.h> #include <min-max.h> @@ -91,6 +94,10 @@ char *w32_getenv (const char *); # pragma GCC diagnostic ignored "-Wformat-truncation=2" #endif +#if !defined O_PATH && !defined WINDOWSNT +# define O_PATH O_SEARCH +#endif + /* Name used to invoke this program. */ static char const *progname; @@ -1133,24 +1140,74 @@ process_grouping (void) #ifdef SOCKETS_IN_FILE_SYSTEM -/* Return the file status of NAME, ordinarily a socket. - It should be owned by UID. Return one of the following: - >0 - 'stat' failed with this errno value - -1 - isn't owned by us - 0 - success: none of the above */ +/* A local socket address. The union avoids the need to cast. */ +union local_sockaddr +{ + struct sockaddr_un un; + struct sockaddr sa; +}; + +/* Relative to the directory DIRFD, connect the socket file named ADDR + to the socket S. Return 0 if successful, -1 if DIRFD is not + AT_FDCWD and DIRFD's permissions would allow a symlink attack, an + errno otherwise. */ static int -socket_status (const char *name, uid_t uid) +connect_socket (int dirfd, char const *addr, int s, uid_t uid) { - struct stat statbfr; + int sock_status = 0; - if (stat (name, &statbfr) != 0) - return errno; + union local_sockaddr server; + if (sizeof server.un.sun_path <= strlen (addr)) + return ENAMETOOLONG; + server.un.sun_family = AF_UNIX; + strcpy (server.un.sun_path, addr); - if (statbfr.st_uid != uid) - return -1; + /* If -1, WDFD is not set yet. If nonnegative, WDFD is a file + descriptor for the initial working directory. Otherwise -1 - WDFD is + the error number for the initial working directory. */ + static int wdfd = -1; - return 0; + if (dirfd != AT_FDCWD) + { + /* Fail if DIRFD's permissions are bogus. */ + struct stat st; + if (fstat (dirfd, &st) != 0) + return errno; + if (st.st_uid != uid || (st.st_mode & (S_IWGRP | S_IWOTH))) + return -1; + + if (wdfd == -1) + { + /* Save the initial working directory. */ + wdfd = open (".", O_PATH | O_CLOEXEC); + if (wdfd < 0) + wdfd = -1 - errno; + } + if (wdfd < 0) + return -1 - wdfd; + if (fchdir (dirfd) != 0) + return errno; + + /* Fail if DIRFD has an ACL, which means its permissions are + almost surely bogus. */ + int has_acl = file_has_acl (".", &st); + if (has_acl) + sock_status = has_acl < 0 ? errno : -1; + } + + if (!sock_status) + sock_status = connect (s, &server.sa, sizeof server.un) == 0 ? 0 : errno; + + /* Fail immediately if we cannot change back to the initial working + directory, as that can mess up the rest of execution. */ + if (dirfd != AT_FDCWD && fchdir (wdfd) != 0) + { + message (true, "%s: .: %s\n", progname, strerror (errno)); + exit (EXIT_FAILURE); + } + + return sock_status; } @@ -1327,32 +1384,49 @@ act_on_signals (HSOCKET emacs_socket) } } -/* Create in SOCKNAME (of size SOCKNAMESIZE) a name for a local socket. - The first TMPDIRLEN bytes of SOCKNAME are already initialized to be - the name of a temporary directory. Use UID and SERVER_NAME to - concoct the name. Return the total length of the name if successful, - -1 if it does not fit (and store a truncated name in that case). - Fail if TMPDIRLEN is out of range. */ +enum { socknamesize = sizeof ((struct sockaddr_un *) NULL)->sun_path }; + +/* Given a local socket S, create in *SOCKNAME a name for a local socket + and connect to that socket. The first TMPDIRLEN bytes of *SOCKNAME are + already initialized to be the name of a temporary directory. + Use UID and SERVER_NAME to concoct the name. Return 0 if + successful, -1 if the socket's parent directory is not safe, and an + errno if there is some other problem. */ static int -local_sockname (char *sockname, int socknamesize, int tmpdirlen, - uintmax_t uid, char const *server_name) +local_sockname (int s, char sockname[socknamesize], int tmpdirlen, + uid_t uid, char const *server_name) { /* If ! (0 <= TMPDIRLEN && TMPDIRLEN < SOCKNAMESIZE) the truncated temporary directory name is already in SOCKNAME, so nothing more need be stored. */ - if (0 <= tmpdirlen) - { - int remaining = socknamesize - tmpdirlen; - if (0 < remaining) - { - int suffixlen = snprintf (&sockname[tmpdirlen], remaining, - "/emacs%"PRIuMAX"/%s", uid, server_name); - if (0 <= suffixlen && suffixlen < remaining) - return tmpdirlen + suffixlen; - } - } - return -1; + if (! (0 <= tmpdirlen && tmpdirlen < socknamesize)) + return ENAMETOOLONG; + + /* Put the full address name into the buffer, since the caller might + need it for diagnostics. But don't overrun the buffer. */ + uintmax_t uidmax = uid; + int emacsdirlen; + int suffixlen = snprintf (sockname + tmpdirlen, socknamesize - tmpdirlen, + "/emacs%"PRIuMAX"%n/%s", uidmax, &emacsdirlen, + server_name); + if (! (0 <= suffixlen && suffixlen < socknamesize - tmpdirlen)) + return ENAMETOOLONG; + + /* Make sure the address's parent directory is not a symlink and is + this user's directory and does not let others write to it; this + fends off some symlink attacks. To avoid races, keep the parent + directory open while checking. */ + char *emacsdirend = sockname + tmpdirlen + emacsdirlen; + *emacsdirend = '\0'; + int dir = openat (AT_FDCWD, sockname, + O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + *emacsdirend = '/'; + if (dir < 0) + return errno; + int sock_status = connect_socket (dir, server_name, s, uid); + close (dir); + return sock_status; } /* Create a local socket for SERVER_NAME and connect it to Emacs. If @@ -1363,28 +1437,43 @@ local_sockname (char *sockname, int socknamesize, int tmpdirlen, static HSOCKET set_local_socket (char const *server_name) { - union { - struct sockaddr_un un; - struct sockaddr sa; - } server = {{ .sun_family = AF_UNIX }}; + union local_sockaddr server; + int sock_status; char *sockname = server.un.sun_path; - enum { socknamesize = sizeof server.un.sun_path }; int tmpdirlen = -1; int socknamelen = -1; uid_t uid = geteuid (); bool tmpdir_used = false; + int s = cloexec_socket (AF_UNIX, SOCK_STREAM, 0); + if (s < 0) + { + message (true, "%s: can't create socket: %s\n", + progname, strerror (errno)); + fail (); + } if (strchr (server_name, '/') || (ISSLASH ('\\') && strchr (server_name, '\\'))) - socknamelen = snprintf (sockname, socknamesize, "%s", server_name); + { + socknamelen = snprintf (sockname, socknamesize, "%s", server_name); + sock_status = (0 <= socknamelen && socknamelen < socknamesize + ? connect_socket (AT_FDCWD, sockname, s, 0) + : ENAMETOOLONG); + } else { /* socket_name is a file name component. */ + sock_status = ENOENT; char const *xdg_runtime_dir = egetenv ("XDG_RUNTIME_DIR"); if (xdg_runtime_dir) - socknamelen = snprintf (sockname, socknamesize, "%s/emacs/%s", - xdg_runtime_dir, server_name); - else + { + socknamelen = snprintf (sockname, socknamesize, "%s/emacs/%s", + xdg_runtime_dir, server_name); + sock_status = (0 <= socknamelen && socknamelen < socknamesize + ? connect_socket (AT_FDCWD, sockname, s, 0) + : ENAMETOOLONG); + } + if (sock_status == ENOENT) { char const *tmpdir = egetenv ("TMPDIR"); if (tmpdir) @@ -1403,23 +1492,24 @@ set_local_socket (char const *server_name) if (tmpdirlen < 0) tmpdirlen = snprintf (sockname, socknamesize, "/tmp"); } - socknamelen = local_sockname (sockname, socknamesize, tmpdirlen, + sock_status = local_sockname (s, sockname, tmpdirlen, uid, server_name); tmpdir_used = true; } } - if (! (0 <= socknamelen && socknamelen < socknamesize)) + if (sock_status == 0) + return s; + + if (sock_status == ENAMETOOLONG) { message (true, "%s: socket-name %s... too long\n", progname, sockname); fail (); } - /* See if the socket exists, and if it's owned by us. */ - int sock_status = socket_status (sockname, uid); - if (sock_status) + if (tmpdir_used) { - /* Failing that, see if LOGNAME or USER exist and differ from + /* See whether LOGNAME or USER exist and differ from our euid. If so, look for a socket based on the UID associated with the name. This is reminiscent of the logic that init_editfns uses to set the global Vuser_full_name. */ @@ -1436,48 +1526,26 @@ set_local_socket (char const *server_name) if (pw && pw->pw_uid != uid) { /* We're running under su, apparently. */ - socknamelen = local_sockname (sockname, socknamesize, tmpdirlen, + sock_status = local_sockname (s, sockname, tmpdirlen, pw->pw_uid, server_name); - if (socknamelen < 0) + if (sock_status == 0) + return s; + if (sock_status == ENAMETOOLONG) { message (true, "%s: socket-name %s... too long\n", progname, sockname); exit (EXIT_FAILURE); } - - sock_status = socket_status (sockname, uid); } } } - if (sock_status == 0) - { - HSOCKET s = cloexec_socket (AF_UNIX, SOCK_STREAM, 0); - if (s < 0) - { - message (true, "%s: socket: %s\n", progname, strerror (errno)); - return INVALID_SOCKET; - } - if (connect (s, &server.sa, sizeof server.un) != 0) - { - message (true, "%s: connect: %s\n", progname, strerror (errno)); - CLOSE_SOCKET (s); - return INVALID_SOCKET; - } + close (s); - struct stat connect_stat; - if (fstat (s, &connect_stat) != 0) - sock_status = errno; - else if (connect_stat.st_uid == uid) - return s; - else - sock_status = -1; - - CLOSE_SOCKET (s); - } - - if (sock_status < 0) - message (true, "%s: Invalid socket owner\n", progname); + if (sock_status == -1) + message (true, + "%s: Invalid permissions on parent directory of socket: %s\n", + progname, sockname); else if (sock_status == ENOENT) { if (tmpdir_used) @@ -1507,7 +1575,7 @@ set_local_socket (char const *server_name) } } else - message (true, "%s: can't stat %s: %s\n", + message (true, "%s: can't connect to %s: %s\n", progname, sockname, strerror (sock_status)); return INVALID_SOCKET; |