unix-socket: handle long socket pathnames
authorJeff King <peff@peff.net>
Tue, 10 Jan 2012 04:44:30 +0000 (23:44 -0500)
committerJunio C Hamano <gitster@pobox.com>
Tue, 10 Jan 2012 18:10:36 +0000 (10:10 -0800)
On many systems, the sockaddr_un.sun_path field is quite
small. Even on Linux, it is only 108 characters. A user of
the credential-cache daemon can easily surpass this,
especially if their home directory is in a deep directory
tree (since the default location expands ~/.git-credentials).

We can hack around this in the unix-socket.[ch] code by
doing a chdir() to the enclosing directory, feeding the
relative basename to the socket functions, and then
restoring the working directory.

This introduces several new possible error cases for
creating a socket, including an irrecoverable one in the
case that we can't restore the working directory. In the
case of the credential-cache code, we could perhaps get away
with simply chdir()-ing to the socket directory and never
coming back. However, I'd rather do it at the lower level
for a few reasons:

  1. It keeps the hackery behind an opaque interface instead
     of polluting the main program logic.

  2. A hack in credential-cache won't help any unix-socket
     users who come along later.

  3. The chdir trickery isn't that likely to fail (basically
     it's only a problem if your cwd is missing or goes away
     while you're running).  And because we only enable the
     hack when we get a too-long name, it can only fail in
     cases that would have failed under the previous code
     anyway.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
unix-socket.c

index 84b1509..7d8bec6 100644 (file)
@@ -9,27 +9,83 @@ static int unix_stream_socket(void)
        return fd;
 }
 
-static void unix_sockaddr_init(struct sockaddr_un *sa, const char *path)
+static int chdir_len(const char *orig, int len)
+{
+       char *path = xmemdupz(orig, len);
+       int r = chdir(path);
+       free(path);
+       return r;
+}
+
+struct unix_sockaddr_context {
+       char orig_dir[PATH_MAX];
+};
+
+static void unix_sockaddr_cleanup(struct unix_sockaddr_context *ctx)
+{
+       if (!ctx->orig_dir[0])
+               return;
+       /*
+        * If we fail, we can't just return an error, since we have
+        * moved the cwd of the whole process, which could confuse calling
+        * code.  We are better off to just die.
+        */
+       if (chdir(ctx->orig_dir) < 0)
+               die("unable to restore original working directory");
+}
+
+static int unix_sockaddr_init(struct sockaddr_un *sa, const char *path,
+                             struct unix_sockaddr_context *ctx)
 {
        int size = strlen(path) + 1;
-       if (size > sizeof(sa->sun_path))
-               die("socket path is too long to fit in sockaddr");
+
+       ctx->orig_dir[0] = '\0';
+       if (size > sizeof(sa->sun_path)) {
+               const char *slash = find_last_dir_sep(path);
+               const char *dir;
+
+               if (!slash) {
+                       errno = ENAMETOOLONG;
+                       return -1;
+               }
+
+               dir = path;
+               path = slash + 1;
+               size = strlen(path) + 1;
+               if (size > sizeof(sa->sun_path)) {
+                       errno = ENAMETOOLONG;
+                       return -1;
+               }
+
+               if (!getcwd(ctx->orig_dir, sizeof(ctx->orig_dir))) {
+                       errno = ENAMETOOLONG;
+                       return -1;
+               }
+               if (chdir_len(dir, slash - dir) < 0)
+                       return -1;
+       }
+
        memset(sa, 0, sizeof(*sa));
        sa->sun_family = AF_UNIX;
        memcpy(sa->sun_path, path, size);
+       return 0;
 }
 
 int unix_stream_connect(const char *path)
 {
        int fd;
        struct sockaddr_un sa;
+       struct unix_sockaddr_context ctx;
 
-       unix_sockaddr_init(&sa, path);
+       if (unix_sockaddr_init(&sa, path, &ctx) < 0)
+               return -1;
        fd = unix_stream_socket();
        if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+               unix_sockaddr_cleanup(&ctx);
                close(fd);
                return -1;
        }
+       unix_sockaddr_cleanup(&ctx);
        return fd;
 }
 
@@ -37,20 +93,25 @@ int unix_stream_listen(const char *path)
 {
        int fd;
        struct sockaddr_un sa;
+       struct unix_sockaddr_context ctx;
 
-       unix_sockaddr_init(&sa, path);
+       if (unix_sockaddr_init(&sa, path, &ctx) < 0)
+               return -1;
        fd = unix_stream_socket();
 
        unlink(path);
        if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+               unix_sockaddr_cleanup(&ctx);
                close(fd);
                return -1;
        }
 
        if (listen(fd, 5) < 0) {
+               unix_sockaddr_cleanup(&ctx);
                close(fd);
                return -1;
        }
 
+       unix_sockaddr_cleanup(&ctx);
        return fd;
 }