[PATCH 2/2] credential-cache--daemon: do not take over a running daemon's socket
- From
Farid Zakaria <farid.m.zakaria@gmail.com>
- Date
- Nov 1, 2026, 23:29 UTC
- Message-ID
- <20261101-fz-credential-cache-race-v1-2-28f0bac51e11@gmail.com>
- In-Reply-To
- <20261101-fz-credential-cache-race-v1-0-28f0bac51e11@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.
The fix is to create 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. A daemon that finds one prints "ok" and exits, and its client talks to the running daemon. Register the socket for removal only after binding it, so a daemon that loses never removes the other one's socket.
Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com> --- builtin/credential-cache--daemon.c | 29 +++++++++++++++++++++-------- t/t0301-credential-cache.sh | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-)
diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c index 0dfa11e2ec..de6720c276 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,26 @@ 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) + /* + * Two clients may each start a daemon at the same time. Bind only + * if no other daemon is listening yet; otherwise leave the socket + * to that one, and our client will talk to it. + */ + ret = unix_ss_create(socket_path, &opts, -1, &server_socket); + if (ret == -2) { + printf("ok\n"); + return; + } + if (ret < 0) die_errno("unable to bind to '%s'", socket_path); + /* Remove the socket at exit only now that it is ours. */ + socket_file = register_tempfile(socket_path); + printf("ok\n"); fclose(stdout); if (!debug) { @@ -244,10 +259,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 +319,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 +346,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