From: Farid Zakaria Date: Wed, 04 Nov 2026 19:53:16 GMT Subject: [PATCH v2 3/3] credential-cache--daemon: do not take over a running daemon's socket Message-ID: <20261104-fz-credential-cache-race-v2-3-ddccd0831eba@gmail.com> In-Reply-To: <20261104-fz-credential-cache-race-v2-0-ddccd0831eba@gmail.com> When no daemon is running, two "git credential-cache store" run at the same time each try to start one. unix_stream_listen() removes whatever is at the socket path before it binds, so the second daemon takes over the socket of the first. The first daemon keeps running, but no client can reach it: - the credential its client stored is lost; - "git credential-cache exit" does not stop it, and it keeps the password in memory until it times out; - when it does exit, it removes the socket of the daemon that is still running, losing that daemon's credentials too. Fix this by creating the socket with unix_ss_create(), which the simple-ipc server already uses. It takes a lock and binds only if no other server is listening on the socket. The lock is held only while binding, so a stale lockfile left behind by a crashed daemon is unlikely to need manual cleanup. Signed-off-by: Farid Zakaria --- builtin/credential-cache--daemon.c | 28 ++++++++++++++++++++-------- t/t0301-credential-cache.sh | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c index 9e6ed9c922..03d314f19f 100644 --- a/builtin/credential-cache--daemon.c +++ b/builtin/credential-cache--daemon.c @@ -11,6 +11,7 @@ #include "tempfile.h" #include "credential.h" #include "unix-socket.h" +#include "unix-stream-server.h" struct credential_cache_entry { struct credential item; @@ -231,12 +232,25 @@ static int serve_cache_loop(int fd) static void serve_cache(const char *socket_path, int debug) { struct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT; - int fd; + struct unix_ss_socket *server_socket; + struct tempfile *socket_file; + int ret; - fd = unix_stream_listen(socket_path, &opts); - if (fd < 0) + ret = unix_ss_create(socket_path, &opts, -1, &server_socket); + if (ret == -2) { + /* + * Another daemon is already listening on the socket. "ok" + * tells our client that a daemon is ready, after which the + * client connects to the socket and talks to that daemon. + */ + printf("ok\n"); + exit(0); + } + if (ret < 0) die_errno("unable to bind to '%s'", socket_path); + socket_file = register_tempfile(socket_path); + printf("ok\n"); fclose(stdout); if (!debug) { @@ -244,10 +258,11 @@ static void serve_cache(const char *socket_path, int debug) die_errno("unable to point stderr to /dev/null"); } - while (serve_cache_loop(fd)) + while (serve_cache_loop(server_socket->fd_socket)) ; /* nothing */ - close(fd); + delete_tempfile(&socket_file); + unix_ss_free(server_socket); } static const char permissions_advice[] = N_( @@ -303,7 +318,6 @@ int cmd_credential_cache_daemon(int argc, const char *prefix, struct repository *repo UNUSED) { - struct tempfile *socket_file; const char *socket_path; int ignore_sighup = 0; static const char *usage[] = { @@ -331,13 +345,11 @@ int cmd_credential_cache_daemon(int argc, die("socket directory must be an absolute path"); init_socket_directory(socket_path); - socket_file = register_tempfile(socket_path); if (ignore_sighup) signal(SIGHUP, SIG_IGN); serve_cache(socket_path, debug); - delete_tempfile(&socket_file); return 0; } diff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh index 6f7cfd9e33..8e07e55520 100755 --- a/t/t0301-credential-cache.sh +++ b/t/t0301-credential-cache.sh @@ -51,6 +51,29 @@ test_expect_success 'socket defaults to ~/.cache/git/credential/socket' ' test_path_is_socket "$HOME/.cache/git/credential/socket" ' +test_expect_success 'a second daemon does not take over the socket' ' + test_when_finished "git credential-cache exit" && + check approve cache <<-\EOF && + protocol=https + host=example.com + username=store-user + password=store-pass + EOF + git credential-cache--daemon "$HOME/.cache/git/credential/socket" >out && + echo ok >expect && + test_cmp expect out && + check fill cache <<-\EOF + protocol=https + host=example.com + -- + protocol=https + host=example.com + username=store-user + password=store-pass + -- + EOF +' + XDG_CACHE_HOME="$HOME/xdg" export XDG_CACHE_HOME # test behavior when XDG_CACHE_HOME is set -- 2.55.0