{"thread":{"id":"66742","subject":"[PATCH 0/2] credential-cache: fix two races when daemons start at once","startedAt":"2026-11-01T23:29:24Z","lastAt":"2026-11-04T19:53:16Z","messageCount":14,"participants":["Farid Zakaria","Patrick Steinhardt","Junio C Hamano","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"557104","messageId":"20261101-fz-credential-cache-race-v1-0-28f0bac51e11@gmail.com","threadId":"66742","inReplyTo":null,"subject":"[PATCH 0/2] credential-cache: fix two races when daemons start at once","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-01T23:29:24Z","receivedAt":"2026-11-01T23:29:24Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"When no credential-cache daemon is running, two \"git credential-cache\nstore\" which run at the same time each try to start a daemon.\n\nThe second daemon unlinks the first one's socket and binds its own,\nwhich means any credential stored by the first one is lost.\n\nBy products of this race:\n  - \"git credential-cache get\" does not return that credential;\n  - \"git credential-cache exit\" stops only the second daemon, and the\n   first keeps the password in memory until it expires;\n  - when the first daemon exits, its tempfile handler removes\n   the socket of the second daemon, whose credentials are then lost\n   too.\n\nThis can happen whenever two git commands store a password at the\nsame moment, for example parallel submodule fetches over HTTPS.\n\nI found this with Rewind [1], a deterministic VM I am building that\nreplays a program under many thread schedules until one fails. \n\nContext from some history spelunking with an AI:\n  Jeff noted in 2021 that the daemon's socket setup is racy [2], and\n  expected the worst case to be a daemon that waits 30 seconds for data\n  before it exits. That holds only if the losing daemon gets no\n  credential, but its own client stores one right after it starts.\n\n[1] https://rewindvm.dev/\n[2] https://lore.kernel.org/git/YCK5K%2FnK4tGnTvou@coredump.intra.peff.net/\n\n---\nFarid Zakaria (2):\n      credential-cache--daemon: allow a concurrently created socket dir\n      credential-cache--daemon: do not take over a running daemon's socket\n\n builtin/credential-cache--daemon.c | 43 +++++++++++++++++++++++++++++---------\n t/t0301-credential-cache.sh        | 23 ++++++++++++++++++++\n 2 files changed, 56 insertions(+), 10 deletions(-)\n\n\n---\nbase-commit: 50fe5cc63f9cd6e89b87a2c9ccf96a444f4cfdb7\nchange-id: 20261101-fz-credential-cache-race-4504110c6415\n\n\n"},{"id":"557105","messageId":"20261101-fz-credential-cache-race-v1-1-28f0bac51e11@gmail.com","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-0-28f0bac51e11@gmail.com","subject":"[PATCH 1/2] credential-cache--daemon: allow a concurrently created socket dir","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-01T23:29:25Z","receivedAt":"2026-11-01T23:29:25Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"When no daemon is running, two \"git credential-cache store\" run at the\nsame time each try to start one. If the socket directory does not exist\nyet, both daemons try to create it. The one that comes second dies:\n\n    fatal: unable to mkdir '<home>/.cache/git/credential': File exists\n\nThat client then fails with \"cache daemon did not start\".\n\nThe fix is to ignore EEXIST from mkdir(), and check the directory's\npermissions the same way as for one that already existed.\n\nSigned-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>\n---\n builtin/credential-cache--daemon.c | 14 ++++++++++++--\n 1 file changed, 12 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\nindex 65cc619bec..0dfa11e2ec 100644\n--- a/builtin/credential-cache--daemon.c\n+++ b/builtin/credential-cache--daemon.c\n@@ -273,8 +273,18 @@ static void init_socket_directory(const char *path)\n \t\t */\n \t\tif (safe_create_leading_directories_const(the_repository, dir) < 0)\n \t\t\tdie_errno(\"unable to create directories for '%s'\", dir);\n-\t\tif (mkdir(dir, 0700) < 0)\n-\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n+\t\tif (mkdir(dir, 0700) < 0) {\n+\t\t\tif (errno != EEXIST)\n+\t\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n+\t\t\t/*\n+\t\t\t * Another daemon created it first; check it like one\n+\t\t\t * that already existed.\n+\t\t\t */\n+\t\t\tif (stat(dir, &st))\n+\t\t\t\tdie_errno(\"unable to stat '%s'\", dir);\n+\t\t\tif (st.st_mode & 077)\n+\t\t\t\tdie(_(permissions_advice), dir);\n+\t\t}\n \t}\n \n \tif (chdir(dir))\n\n-- \n2.55.0\n\n\n"},{"id":"557106","messageId":"20261101-fz-credential-cache-race-v1-2-28f0bac51e11@gmail.com","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-0-28f0bac51e11@gmail.com","subject":"[PATCH 2/2] credential-cache--daemon: do not take over a running daemon's socket","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-01T23:29:26Z","receivedAt":"2026-11-01T23:29:26Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"When no daemon is running, two \"git credential-cache store\" run at the\nsame time each try to start one. unix_stream_listen() removes whatever\nis at the socket path before it binds, so the second daemon takes over\nthe socket of the first. The first daemon keeps running, but no client\ncan reach it:\n\n - the credential its client stored is lost;\n\n - \"git credential-cache exit\" does not stop it, and it keeps the\n   password in memory until it times out;\n\n - when it does exit, it removes the socket of the daemon that is\n   still running, losing that daemon's credentials too.\n\nThe fix is to create the socket with unix_ss_create(), which the\nsimple-ipc server already uses. It takes a lock and binds only if no\nother server is listening on the socket. A daemon that finds one prints\n\"ok\" and exits, and its client talks to the running daemon. Register\nthe socket for removal only after binding it, so a daemon that loses\nnever removes the other one's socket.\n\nSigned-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>\n---\n builtin/credential-cache--daemon.c | 29 +++++++++++++++++++++--------\n t/t0301-credential-cache.sh        | 23 +++++++++++++++++++++++\n 2 files changed, 44 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\nindex 0dfa11e2ec..de6720c276 100644\n--- a/builtin/credential-cache--daemon.c\n+++ b/builtin/credential-cache--daemon.c\n@@ -11,6 +11,7 @@\n #include \"tempfile.h\"\n #include \"credential.h\"\n #include \"unix-socket.h\"\n+#include \"unix-stream-server.h\"\n \n struct credential_cache_entry {\n \tstruct credential item;\n@@ -231,12 +232,26 @@ static int serve_cache_loop(int fd)\n static void serve_cache(const char *socket_path, int debug)\n {\n \tstruct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;\n-\tint fd;\n+\tstruct unix_ss_socket *server_socket;\n+\tstruct tempfile *socket_file;\n+\tint ret;\n \n-\tfd = unix_stream_listen(socket_path, &opts);\n-\tif (fd < 0)\n+\t/*\n+\t * Two clients may each start a daemon at the same time. Bind only\n+\t * if no other daemon is listening yet; otherwise leave the socket\n+\t * to that one, and our client will talk to it.\n+\t */\n+\tret = unix_ss_create(socket_path, &opts, -1, &server_socket);\n+\tif (ret == -2) {\n+\t\tprintf(\"ok\\n\");\n+\t\treturn;\n+\t}\n+\tif (ret < 0)\n \t\tdie_errno(\"unable to bind to '%s'\", socket_path);\n \n+\t/* Remove the socket at exit only now that it is ours. */\n+\tsocket_file = register_tempfile(socket_path);\n+\n \tprintf(\"ok\\n\");\n \tfclose(stdout);\n \tif (!debug) {\n@@ -244,10 +259,11 @@ static void serve_cache(const char *socket_path, int debug)\n \t\t\tdie_errno(\"unable to point stderr to /dev/null\");\n \t}\n \n-\twhile (serve_cache_loop(fd))\n+\twhile (serve_cache_loop(server_socket->fd_socket))\n \t\t; /* nothing */\n \n-\tclose(fd);\n+\tdelete_tempfile(&socket_file);\n+\tunix_ss_free(server_socket);\n }\n \n static const char permissions_advice[] = N_(\n@@ -303,7 +319,6 @@ int cmd_credential_cache_daemon(int argc,\n \t\t\t\tconst char *prefix,\n \t\t\t\tstruct repository *repo UNUSED)\n {\n-\tstruct tempfile *socket_file;\n \tconst char *socket_path;\n \tint ignore_sighup = 0;\n \tstatic const char *usage[] = {\n@@ -331,13 +346,11 @@ int cmd_credential_cache_daemon(int argc,\n \t\tdie(\"socket directory must be an absolute path\");\n \n \tinit_socket_directory(socket_path);\n-\tsocket_file = register_tempfile(socket_path);\n \n \tif (ignore_sighup)\n \t\tsignal(SIGHUP, SIG_IGN);\n \n \tserve_cache(socket_path, debug);\n-\tdelete_tempfile(&socket_file);\n \n \treturn 0;\n }\ndiff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\nindex 6f7cfd9e33..8e07e55520 100755\n--- a/t/t0301-credential-cache.sh\n+++ b/t/t0301-credential-cache.sh\n@@ -51,6 +51,29 @@ test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n \ttest_path_is_socket \"$HOME/.cache/git/credential/socket\"\n '\n \n+test_expect_success 'a second daemon does not take over the socket' '\n+\ttest_when_finished \"git credential-cache exit\" &&\n+\tcheck approve cache <<-\\EOF &&\n+\tprotocol=https\n+\thost=example.com\n+\tusername=store-user\n+\tpassword=store-pass\n+\tEOF\n+\tgit credential-cache--daemon \"$HOME/.cache/git/credential/socket\" >out &&\n+\techo ok >expect &&\n+\ttest_cmp expect out &&\n+\tcheck fill cache <<-\\EOF\n+\tprotocol=https\n+\thost=example.com\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=store-user\n+\tpassword=store-pass\n+\t--\n+\tEOF\n+'\n+\n XDG_CACHE_HOME=\"$HOME/xdg\"\n export XDG_CACHE_HOME\n # test behavior when XDG_CACHE_HOME is set\n\n-- \n2.55.0\n\n\n"},{"id":"557273","messageId":"aumUxF1uVtVC4qYj@pks.im","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-1-28f0bac51e11@gmail.com","subject":"Re: [PATCH 1/2] credential-cache--daemon: allow a concurrently created socket dir","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-11-03T08:01:08Z","receivedAt":"2026-11-03T08:01:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sun, Nov 01, 2026 at 03:29:25PM -0800, Farid Zakaria wrote:\n> diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\n> index 65cc619bec..0dfa11e2ec 100644\n> --- a/builtin/credential-cache--daemon.c\n> +++ b/builtin/credential-cache--daemon.c\n> @@ -273,8 +273,18 @@ static void init_socket_directory(const char *path)\n>  \t\t */\n>  \t\tif (safe_create_leading_directories_const(the_repository, dir) < 0)\n>  \t\t\tdie_errno(\"unable to create directories for '%s'\", dir);\n\nIt's quite curious that we use `safe_create_leading_directories()` here.\nAs documented in \"path.h\", the `safe_...()` functions should only ever\nbe used for creating directories in the \".git\" directory itself. In\nparticular, applying \"core.sharedRepostiory\" to the leading directories\nof our credential daemon is never going to result in anything good at\nall.\n\nSo we should call `safe_create_leadingdirectories_no_share_const()`\ninstead, which does not apply \"core.sharedRepository\". I know that's not\nintroduced by your patch, but would you mind maybe fixing that in a\npreparatory commit?\n\n> -\t\tif (mkdir(dir, 0700) < 0)\n> -\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n> +\t\tif (mkdir(dir, 0700) < 0) {\n> +\t\t\tif (errno != EEXIST)\n> +\t\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n\nMakes sense.\n\n> +\t\t\t/*\n> +\t\t\t * Another daemon created it first; check it like one\n> +\t\t\t * that already existed.\n> +\t\t\t */\n> +\t\t\tif (stat(dir, &st))\n> +\t\t\t\tdie_errno(\"unable to stat '%s'\", dir);\n> +\t\t\tif (st.st_mode & 077)\n> +\t\t\t\tdie(_(permissions_advice), dir);\n> +\t\t}\n\nA bit of a shame that we duplicate this logic, but I don't see an\nobvious way for how to avoid that.\n\nPatrick\n\n"},{"id":"557274","messageId":"aumUyl6MQa43OFBs@pks.im","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-2-28f0bac51e11@gmail.com","subject":"Re: [PATCH 2/2] credential-cache--daemon: do not take over a running daemon's socket","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-11-03T08:01:14Z","receivedAt":"2026-11-03T08:01:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sun, Nov 01, 2026 at 03:29:26PM -0800, Farid Zakaria wrote:\n> When no daemon is running, two \"git credential-cache store\" run at the\n> same time each try to start one. unix_stream_listen() removes whatever\n> is at the socket path before it binds, so the second daemon takes over\n> the socket of the first. The first daemon keeps running, but no client\n> can reach it:\n> \n>  - the credential its client stored is lost;\n> \n>  - \"git credential-cache exit\" does not stop it, and it keeps the\n>    password in memory until it times out;\n> \n>  - when it does exit, it removes the socket of the daemon that is\n>    still running, losing that daemon's credentials too.\n> \n> The fix is to create the socket with unix_ss_create(), which the\n> simple-ipc server already uses.\n\nWe usually write commit messages as if instructing the code to change.\nSo: \"Fix that by using `unix_ss_create()` instead...\".\n\n> diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\n> index 0dfa11e2ec..de6720c276 100644\n> --- a/builtin/credential-cache--daemon.c\n> +++ b/builtin/credential-cache--daemon.c\n> @@ -231,12 +232,26 @@ static int serve_cache_loop(int fd)\n>  static void serve_cache(const char *socket_path, int debug)\n>  {\n>  \tstruct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;\n> -\tint fd;\n> +\tstruct unix_ss_socket *server_socket;\n> +\tstruct tempfile *socket_file;\n> +\tint ret;\n>  \n> -\tfd = unix_stream_listen(socket_path, &opts);\n> -\tif (fd < 0)\n> +\t/*\n> +\t * Two clients may each start a daemon at the same time. Bind only\n> +\t * if no other daemon is listening yet; otherwise leave the socket\n> +\t * to that one, and our client will talk to it.\n> +\t */\n\nThis explains what you do without giving much context why you do it, so\nthis comment is somewhat useless. Most importantly...\n\n> +\tret = unix_ss_create(socket_path, &opts, -1, &server_socket);\n> +\tif (ret == -2) {\n> +\t\tprintf(\"ok\\n\");\n> +\t\treturn;\n\n... you mention that you write \"ok\" in your commit message, but you\ndon't explain why that's the right thing to do. So why should the daemon\npretend that everything was alright in the case where it fails to start?\n\n> +\t}\n> +\tif (ret < 0)\n>  \t\tdie_errno(\"unable to bind to '%s'\", socket_path);\n>  \n> +\t/* Remove the socket at exit only now that it is ours. */\n> +\tsocket_file = register_tempfile(socket_path);\n\nThis comment doesn't really add much value, if you ask me.\n\nPatrick\n\n"},{"id":"557340","messageId":"xmqqik2dakw2.fsf@gitster.g","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-2-28f0bac51e11@gmail.com","subject":"Re: [PATCH 2/2] credential-cache--daemon: do not take over a running daemon's socket","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-11-03T17:24:13Z","receivedAt":"2026-11-03T17:24:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Farid Zakaria <farid.m.zakaria@gmail.com> writes:\n\n> +\t/* Remove the socket at exit only now that it is ours. */\n> +\tsocket_file = register_tempfile(socket_path);\n> +\n>  \tprintf(\"ok\\n\");\n>  \tfclose(stdout);\n>  \tif (!debug) {\n> @@ -244,10 +259,11 @@ static void serve_cache(const char *socket_path, int debug)\n>  \t\t\tdie_errno(\"unable to point stderr to /dev/null\");\n>  \t}\n>  \n> -\twhile (serve_cache_loop(fd))\n> +\twhile (serve_cache_loop(server_socket->fd_socket))\n>  \t\t; /* nothing */\n>  \n> -\tclose(fd);\n> +\tdelete_tempfile(&socket_file);\n> +\tunix_ss_free(server_socket);\n>  }\n\nIs it sane to mix unix_ss_* API with tempfile like this?  For Unix\ndomain sockets managed by the unix_ss_* API, isn't\nregister_tempfile() unnecessary and architecturally undesirable?\n\nIf the daemon exits abruptly without cleaning up its socket, it is\nentirely harmless.  The next time a daemon starts, unix_ss_create()\nwill find that the socket is stale (is_another_server_alive() will\nreturn 0), and it will safely unlink() the stale socket before\nbinding.\n\nE.g., fsmonitor--daemon.c calls ipc_server_init_async(), which is\nimplemented in ipc-unix-socket.c by calling setup_listener_socket(),\nwhich in turn calls create_listener_socket(), which in turn finally\ncalls unix-stream-server.c:unix_ss_create().  In that function, we\ncheck if somebody else is listening or if the on-filesystem socket\nis stale, unlink and rebind by calling unix_stream_listen(), and\ncontinue.\n\nBy invoking register_tempfile(), the Unix socket would be managed\nby the tempfile API.  At normal exit, delete_tempfile(&socket_file)\nblindly unlinks the socket file.  If the daemon is killed abruptly by\na signal (e.g., SIGTERM), the tempfile API's signal handler will\nsimilarly unlink the socket blindly.  Doesn't this completely bypass\nthe safety built into unix_ss_free(), though?  The API deliberately\ncalls unix_ss_was_stolen() (which checks the inode) to prevent a\ndying daemon from unlinking a socket that has since been taken over\nby a new daemon, but delete_tempfile() has no way of knowing that.\nIt runs first (or runs in the signal handler) and blindly unlinks\nthe socket, defeating the safety check, no?\n\n"},{"id":"557397","messageId":"20261104025835.GB1492379@coredump.intra.peff.net","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-1-28f0bac51e11@gmail.com","subject":"Re: [PATCH 1/2] credential-cache--daemon: allow a concurrently created socket dir","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-11-04T02:58:35Z","receivedAt":"2026-11-04T02:58:35Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Nov 01, 2026 at 03:29:25PM -0800, Farid Zakaria wrote:\n\n> When no daemon is running, two \"git credential-cache store\" run at the\n> same time each try to start one. If the socket directory does not exist\n> yet, both daemons try to create it. The one that comes second dies:\n> \n>     fatal: unable to mkdir '<home>/.cache/git/credential': File exists\n> \n> That client then fails with \"cache daemon did not start\".\n> \n> The fix is to ignore EEXIST from mkdir(), and check the directory's\n> permissions the same way as for one that already existed.\n\nMakes sense. This is what safe_create_leading_directories() does for all\nof the other path components. So of course I wondered why we bothered to\ndo this last one ourselves, but the comment (which is just out of the\ncontext of your patch) explains it:\n\n                /*\n                 * We must be sure to create the directory with the correct mode,\n                 * not just chmod it after the fact; otherwise, there is a race\n                 * condition in which somebody can chdir to it, sleep, then try to open\n                 * our protected socket.\n                 */\n\nIf we find the directory already created, then we are subject to the\nsame race, but it is reasonable to assume that whoever created it did so\nin the correct non-racy way.\n\n-Peff\n\n"},{"id":"557398","messageId":"20261104030753.GC1492379@coredump.intra.peff.net","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-2-28f0bac51e11@gmail.com","subject":"Re: [PATCH 2/2] credential-cache--daemon: do not take over a running daemon's socket","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-11-04T03:07:53Z","receivedAt":"2026-11-04T03:07:53Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Nov 01, 2026 at 03:29:26PM -0800, Farid Zakaria wrote:\n\n> The fix is to create the socket with unix_ss_create(), which the\n> simple-ipc server already uses. It takes a lock and binds only if no\n> other server is listening on the socket. A daemon that finds one prints\n> \"ok\" and exits, and its client talks to the running daemon. Register\n> the socket for removal only after binding it, so a daemon that loses\n> never removes the other one's socket.\n\nOK. One of the reasons the original did not take a lock is that a stale\nlockfile might force the user to clean it up manually (rather than\neverything eventually Just Working).\n\nBut unix_ss_create() only holds the lockfile briefly to make the socket\nacquisition atomic. So I think the risk of stale files is negligible,\nand worth the tradeoff to avoid races.\n\n>  static void serve_cache(const char *socket_path, int debug)\n>  {\n>  \tstruct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;\n> -\tint fd;\n> +\tstruct unix_ss_socket *server_socket;\n> +\tstruct tempfile *socket_file;\n> +\tint ret;\n>  \n> -\tfd = unix_stream_listen(socket_path, &opts);\n> -\tif (fd < 0)\n> +\t/*\n> +\t * Two clients may each start a daemon at the same time. Bind only\n> +\t * if no other daemon is listening yet; otherwise leave the socket\n> +\t * to that one, and our client will talk to it.\n> +\t */\n> +\tret = unix_ss_create(socket_path, &opts, -1, &server_socket);\n> +\tif (ret == -2) {\n> +\t\tprintf(\"ok\\n\");\n> +\t\treturn;\n> +\t}\n\nSo if we found that somebody else beat us to it, we still say \"ok\",\nwhich makes sense: we tell the client that _some_ daemon is ready for\nthem to talk to, even if it's not us.\n\nAnd then conceptually we are done and can exit. The return here does the\nsame thing. I think that's OK, but it might be more obvious to add a\ncomment for the \"ok\" and just call exit(0). I am happy enough either\nway.\n\n> +\t/* Remove the socket at exit only now that it is ours. */\n> +\tsocket_file = register_tempfile(socket_path);\n\nAnd then tempfile handling moves in here from the caller, because we\nmight or might not actually make a socket. Makes sense.\n\n> [...]\n\nThe rest of the code changes look as I'd expect.\n\n> diff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\n> index 6f7cfd9e33..8e07e55520 100755\n> --- a/t/t0301-credential-cache.sh\n> +++ b/t/t0301-credential-cache.sh\n> @@ -51,6 +51,29 @@ test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n>  \ttest_path_is_socket \"$HOME/.cache/git/credential/socket\"\n>  '\n>  \n> +test_expect_success 'a second daemon does not take over the socket' '\n> +\ttest_when_finished \"git credential-cache exit\" &&\n> +\tcheck approve cache <<-\\EOF &&\n> +\tprotocol=https\n> +\thost=example.com\n> +\tusername=store-user\n> +\tpassword=store-pass\n> +\tEOF\n> +\tgit credential-cache--daemon \"$HOME/.cache/git/credential/socket\" >out &&\n> +\techo ok >expect &&\n> +\ttest_cmp expect out &&\n> +\tcheck fill cache <<-\\EOF\n> +\tprotocol=https\n> +\thost=example.com\n> +\t--\n> +\tprotocol=https\n> +\thost=example.com\n> +\tusername=store-user\n> +\tpassword=store-pass\n> +\t--\n> +\tEOF\n> +'\n\nI'd have thought this would pass even before the patch, but perhaps not.\nUsually we rely on the \"credential-cache\" client doing a ping to avoid\nstarting a second daemon, but here we do it explicitly. So probably it\ndid just take over the other one, but nobody would really hit that case\noutside of the race between the client pings and the daemons actually\nstarting.\n\nSo overall, I think the patch looks good.\n\n-Peff\n\n"},{"id":"557484","messageId":"DMMMYT4Q87N3.W2MQQMHI76I6@gmail.com","threadId":"66742","inReplyTo":"aumUxF1uVtVC4qYj@pks.im","subject":"Re: [PATCH 1/2] credential-cache--daemon: allow a concurrently created socket dir","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-04T18:25:46Z","receivedAt":"2026-11-04T18:25:46Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"On Tue Nov 3, 2026 at 12:01 AM PST, Patrick Steinhardt wrote:\n> On Sun, Nov 01, 2026 at 03:29:25PM -0800, Farid Zakaria wrote:\n>> diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\n>> index 65cc619bec..0dfa11e2ec 100644\n>> --- a/builtin/credential-cache--daemon.c\n>> +++ b/builtin/credential-cache--daemon.c\n>> @@ -273,8 +273,18 @@ static void init_socket_directory(const char *path)\n>>  \t\t */\n>>  \t\tif (safe_create_leading_directories_const(the_repository, dir) < 0)\n>>  \t\t\tdie_errno(\"unable to create directories for '%s'\", dir);\n>\n> It's quite curious that we use `safe_create_leading_directories()` here.\n> As documented in \"path.h\", the `safe_...()` functions should only ever\n> be used for creating directories in the \".git\" directory itself. In\n> particular, applying \"core.sharedRepostiory\" to the leading directories\n> of our credential daemon is never going to result in anything good at\n> all.\n>\n> So we should call `safe_create_leadingdirectories_no_share_const()`\n> instead, which does not apply \"core.sharedRepository\". I know that's not\n> introduced by your patch, but would you mind maybe fixing that in a\n> preparatory commit?\n>\n\nSure, I'll add that as the first patch in v2.\n\n>> -\t\tif (mkdir(dir, 0700) < 0)\n>> -\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n>> +\t\tif (mkdir(dir, 0700) < 0) {\n>> +\t\t\tif (errno != EEXIST)\n>> +\t\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n>\n> Makes sense.\n>\n>> +\t\t\t/*\n>> +\t\t\t * Another daemon created it first; check it like one\n>> +\t\t\t * that already existed.\n>> +\t\t\t */\n>> +\t\t\tif (stat(dir, &st))\n>> +\t\t\t\tdie_errno(\"unable to stat '%s'\", dir);\n>> +\t\t\tif (st.st_mode & 077)\n>> +\t\t\t\tdie(_(permissions_advice), dir);\n>> +\t\t}\n>\n> A bit of a shame that we duplicate this logic, but I don't see an\n> obvious way for how to avoid that.\n>\n> Patrick\n\n\n\n"},{"id":"557485","messageId":"DMMN0K2447KN.36D8GZ1EWAVP0@gmail.com","threadId":"66742","inReplyTo":"aumUyl6MQa43OFBs@pks.im","subject":"Re: [PATCH 2/2] credential-cache--daemon: do not take over a running daemon's socket","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-04T18:28:03Z","receivedAt":"2026-11-04T18:28:03Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"On Tue Nov 3, 2026 at 12:01 AM PST, Patrick Steinhardt wrote:\n> On Sun, Nov 01, 2026 at 03:29:26PM -0800, Farid Zakaria wrote:\n>> When no daemon is running, two \"git credential-cache store\" run at the\n>> same time each try to start one. unix_stream_listen() removes whatever\n>> is at the socket path before it binds, so the second daemon takes over\n>> the socket of the first. The first daemon keeps running, but no client\n>> can reach it:\n>> \n>>  - the credential its client stored is lost;\n>> \n>>  - \"git credential-cache exit\" does not stop it, and it keeps the\n>>    password in memory until it times out;\n>> \n>>  - when it does exit, it removes the socket of the daemon that is\n>>    still running, losing that daemon's credentials too.\n>> \n>> The fix is to create the socket with unix_ss_create(), which the\n>> simple-ipc server already uses.\n>\n> We usually write commit messages as if instructing the code to change.\n> So: \"Fix that by using `unix_ss_create()` instead...\".\n>\n\nWhen I sent V2 I will fix this.\n\n>> diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\n>> index 0dfa11e2ec..de6720c276 100644\n>> --- a/builtin/credential-cache--daemon.c\n>> +++ b/builtin/credential-cache--daemon.c\n>> @@ -231,12 +232,26 @@ static int serve_cache_loop(int fd)\n>>  static void serve_cache(const char *socket_path, int debug)\n>>  {\n>>  \tstruct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;\n>> -\tint fd;\n>> +\tstruct unix_ss_socket *server_socket;\n>> +\tstruct tempfile *socket_file;\n>> +\tint ret;\n>>  \n>> -\tfd = unix_stream_listen(socket_path, &opts);\n>> -\tif (fd < 0)\n>> +\t/*\n>> +\t * Two clients may each start a daemon at the same time. Bind only\n>> +\t * if no other daemon is listening yet; otherwise leave the socket\n>> +\t * to that one, and our client will talk to it.\n>> +\t */\n>\n> This explains what you do without giving much context why you do it, so\n> this comment is somewhat useless. Most importantly...\n>\n>> +\tret = unix_ss_create(socket_path, &opts, -1, &server_socket);\n>> +\tif (ret == -2) {\n>> +\t\tprintf(\"ok\\n\");\n>> +\t\treturn;\n>\n> ... you mention that you write \"ok\" in your commit message, but you\n> don't explain why that's the right thing to do. So why should the daemon\n> pretend that everything was alright in the case where it fails to start?\n>\n\nThe client in builtin/credential-cache.c:spawn_daemon() reads the\ndaemon's stdout and dies with \"cache daemon did not start\" unless it\ngets \"ok\"\n\n\"ok\" is part of the protocol between the client and the daemon. The\nclient expects to see \"ok\" and then it connects to the socket.\n\nI can put this reasoning as a comment in the code. \nFollowing Peff's suggestion, I'll also exit(0) there\nto make clear that this process is done.\n\n>> +\t}\n>> +\tif (ret < 0)\n>>  \t\tdie_errno(\"unable to bind to '%s'\", socket_path);\n>>  \n>> +\t/* Remove the socket at exit only now that it is ours. */\n>> +\tsocket_file = register_tempfile(socket_path);\n>\n> This comment doesn't really add much value, if you ask me.\n>\n> Patrick\n\n\n\n"},{"id":"557495","messageId":"20261104-fz-credential-cache-race-v2-0-ddccd0831eba@gmail.com","threadId":"66742","inReplyTo":"20261101-fz-credential-cache-race-v1-0-28f0bac51e11@gmail.com","subject":"[PATCH v2 0/3] credential-cache: fix two races when daemons start at once","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-04T19:53:13Z","receivedAt":"2026-11-04T19:53:13Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"When no credential-cache daemon is running, two \"git credential-cache\nstore\" which run at the same time each try to start a daemon.\n\nThe second daemon unlinks the first one's socket and binds its own,\nwhich means any credential stored by the first one is lost.\n\nBy products of this race:\n  - \"git credential-cache get\" does not return that credential;\n  - \"git credential-cache exit\" stops only the second daemon, and the\n   first keeps the password in memory until it expires;\n  - when the first daemon exits, its tempfile handler removes\n   the socket of the second daemon, whose credentials are then lost\n   too.\n\nThis can happen whenever two git commands store a password at the\nsame moment, for example parallel submodule fetches over HTTPS.\n\nI found this with Rewind [1], a deterministic VM I am building that\nreplays a program under many thread schedules until one fails. \n\nContext from some history spelunking with an AI:\n  Jeff noted in 2021 that the daemon's socket setup is racy [2], and\n  expected the worst case to be a daemon that waits 30 seconds for data\n  before it exits. That holds only if the losing daemon gets no\n  credential, but its own client stores one right after it starts.\n\n[1] https://rewindvm.dev/\n[2] https://lore.kernel.org/git/YCK5K%2FnK4tGnTvou@coredump.intra.peff.net/\n\n---\nChanges in v2:\n- New patch 1: create the socket's leading directories with\n  safe_create_leading_directories_no_share_const(), so that\n  \"core.sharedRepository\" does not apply to them (Patrick).\n- Patch 2: explain in the commit message why a directory created by\n  another daemon can be trusted (Peff).\n- Patch 3: explain in a comment why a daemon that finds another one\n  listening still prints \"ok\", and exit(0) there (Patrick, Peff).\n  Drop comments that only restated the code (Patrick).\n- Reword commit messages in the imperative mood (Patrick).\n- Not changed: the socket is still removed at exit through the\n  tempfile API, which unlinks it without the unix_ss_was_stolen().\n  There was a discussion with Junio about this. I can do that if it's\n  a requirement in a V3.\n- Link to v1: https://patch.msgid.link/20261101-fz-credential-cache-race-v1-0-28f0bac51e11@gmail.com\n\n---\nFarid Zakaria (3):\n      credential-cache--daemon: do not apply core.sharedRepository\n      credential-cache--daemon: allow a concurrently created socket dir\n      credential-cache--daemon: do not take over a running daemon's socket\n\n builtin/credential-cache--daemon.c | 44 ++++++++++++++++++++++++++++----------\n t/t0301-credential-cache.sh        | 23 ++++++++++++++++++++\n 2 files changed, 56 insertions(+), 11 deletions(-)\n\nRange-diff versus v1:\n\n-:  ---------- > 1:  b2b02dd510 credential-cache--daemon: do not apply core.sharedRepository\n1:  5137f72936 ! 2:  e2310083cd credential-cache--daemon: allow a concurrently created socket dir\n    @@ Commit message\n     \n         That client then fails with \"cache daemon did not start\".\n     \n    -    The fix is to ignore EEXIST from mkdir(), and check the directory's\n    -    permissions the same way as for one that already existed.\n    +    Fix this by ignoring EEXIST from mkdir(), and checking the directory's\n    +    permissions the same way as for one that already existed. We create\n    +    the directory with mode 0700 instead of fixing it up with chmod()\n    +    afterwards to avoid a race where another user enters it before the\n    +    chmod(). A directory created by another daemon was made the same way,\n    +    so trusting it once its permissions check out is as safe as trusting\n    +    one that existed before we started.\n     \n         Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>\n     \n      ## builtin/credential-cache--daemon.c ##\n     @@ builtin/credential-cache--daemon.c: static void init_socket_directory(const char *path)\n      \t\t */\n    - \t\tif (safe_create_leading_directories_const(the_repository, dir) < 0)\n    + \t\tif (safe_create_leading_directories_no_share_const(dir) < 0)\n      \t\t\tdie_errno(\"unable to create directories for '%s'\", dir);\n     -\t\tif (mkdir(dir, 0700) < 0)\n     -\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n2:  0e13ea044c ! 3:  54bc9bd3df credential-cache--daemon: do not take over a running daemon's socket\n    @@ Commit message\n          - when it does exit, it removes the socket of the daemon that is\n            still running, losing that daemon's credentials too.\n     \n    -    The fix is to create the socket with unix_ss_create(), which the\n    +    Fix this by creating the socket with unix_ss_create(), which the\n         simple-ipc server already uses. It takes a lock and binds only if no\n    -    other server is listening on the socket. A daemon that finds one prints\n    -    \"ok\" and exits, and its client talks to the running daemon. Register\n    -    the socket for removal only after binding it, so a daemon that loses\n    -    never removes the other one's socket.\n    +    other server is listening on the socket. The lock is held only while\n    +    binding, so a stale lockfile left behind by a crashed daemon is\n    +    unlikely to need manual cleanup.\n     \n         Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>\n     \n    @@ builtin/credential-cache--daemon.c: static int serve_cache_loop(int fd)\n      \n     -\tfd = unix_stream_listen(socket_path, &opts);\n     -\tif (fd < 0)\n    -+\t/*\n    -+\t * Two clients may each start a daemon at the same time. Bind only\n    -+\t * if no other daemon is listening yet; otherwise leave the socket\n    -+\t * to that one, and our client will talk to it.\n    -+\t */\n     +\tret = unix_ss_create(socket_path, &opts, -1, &server_socket);\n     +\tif (ret == -2) {\n    ++\t\t/*\n    ++\t\t * Another daemon is already listening on the socket. \"ok\"\n    ++\t\t * tells our client that a daemon is ready, after which the\n    ++\t\t * client connects to the socket and talks to that daemon.\n    ++\t\t */\n     +\t\tprintf(\"ok\\n\");\n    -+\t\treturn;\n    ++\t\texit(0);\n     +\t}\n     +\tif (ret < 0)\n      \t\tdie_errno(\"unable to bind to '%s'\", socket_path);\n      \n    -+\t/* Remove the socket at exit only now that it is ours. */\n     +\tsocket_file = register_tempfile(socket_path);\n     +\n      \tprintf(\"ok\\n\");\n\n---\nbase-commit: 50fe5cc63f9cd6e89b87a2c9ccf96a444f4cfdb7\nchange-id: 20261101-fz-credential-cache-race-4504110c6415\n\n\n"},{"id":"557496","messageId":"20261104-fz-credential-cache-race-v2-1-ddccd0831eba@gmail.com","threadId":"66742","inReplyTo":"20261104-fz-credential-cache-race-v2-0-ddccd0831eba@gmail.com","subject":"[PATCH v2 1/3] credential-cache--daemon: do not apply core.sharedRepository","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-04T19:53:14Z","receivedAt":"2026-11-04T19:53:14Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"init_socket_directory() creates the leading directories of the socket\npath with safe_create_leading_directories_const(). That function\nhonors \"core.sharedRepository\", which is meant for files inside the\ngit directory. The socket lives in the user's home directory, e.g.\n\"~/.cache/git/credential\", and the repository the daemon happens to be\nstarted from has no say over its permissions.\n\nUse safe_create_leading_directories_no_share_const() instead, which\nignores \"core.sharedRepository\".\n\nSigned-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>\n---\n builtin/credential-cache--daemon.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\nindex 65cc619bec..5cc39cd172 100644\n--- a/builtin/credential-cache--daemon.c\n+++ b/builtin/credential-cache--daemon.c\n@@ -271,7 +271,7 @@ static void init_socket_directory(const char *path)\n \t\t * condition in which somebody can chdir to it, sleep, then try to open\n \t\t * our protected socket.\n \t\t */\n-\t\tif (safe_create_leading_directories_const(the_repository, dir) < 0)\n+\t\tif (safe_create_leading_directories_no_share_const(dir) < 0)\n \t\t\tdie_errno(\"unable to create directories for '%s'\", dir);\n \t\tif (mkdir(dir, 0700) < 0)\n \t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n\n-- \n2.55.0\n\n\n"},{"id":"557497","messageId":"20261104-fz-credential-cache-race-v2-2-ddccd0831eba@gmail.com","threadId":"66742","inReplyTo":"20261104-fz-credential-cache-race-v2-0-ddccd0831eba@gmail.com","subject":"[PATCH v2 2/3] credential-cache--daemon: allow a concurrently created socket dir","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-04T19:53:15Z","receivedAt":"2026-11-04T19:53:15Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"When no daemon is running, two \"git credential-cache store\" run at the\nsame time each try to start one. If the socket directory does not exist\nyet, both daemons try to create it. The one that comes second dies:\n\n    fatal: unable to mkdir '<home>/.cache/git/credential': File exists\n\nThat client then fails with \"cache daemon did not start\".\n\nFix this by ignoring EEXIST from mkdir(), and checking the directory's\npermissions the same way as for one that already existed. We create\nthe directory with mode 0700 instead of fixing it up with chmod()\nafterwards to avoid a race where another user enters it before the\nchmod(). A directory created by another daemon was made the same way,\nso trusting it once its permissions check out is as safe as trusting\none that existed before we started.\n\nSigned-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>\n---\n builtin/credential-cache--daemon.c | 14 ++++++++++++--\n 1 file changed, 12 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\nindex 5cc39cd172..9e6ed9c922 100644\n--- a/builtin/credential-cache--daemon.c\n+++ b/builtin/credential-cache--daemon.c\n@@ -273,8 +273,18 @@ static void init_socket_directory(const char *path)\n \t\t */\n \t\tif (safe_create_leading_directories_no_share_const(dir) < 0)\n \t\t\tdie_errno(\"unable to create directories for '%s'\", dir);\n-\t\tif (mkdir(dir, 0700) < 0)\n-\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n+\t\tif (mkdir(dir, 0700) < 0) {\n+\t\t\tif (errno != EEXIST)\n+\t\t\t\tdie_errno(\"unable to mkdir '%s'\", dir);\n+\t\t\t/*\n+\t\t\t * Another daemon created it first; check it like one\n+\t\t\t * that already existed.\n+\t\t\t */\n+\t\t\tif (stat(dir, &st))\n+\t\t\t\tdie_errno(\"unable to stat '%s'\", dir);\n+\t\t\tif (st.st_mode & 077)\n+\t\t\t\tdie(_(permissions_advice), dir);\n+\t\t}\n \t}\n \n \tif (chdir(dir))\n\n-- \n2.55.0\n\n\n"},{"id":"557498","messageId":"20261104-fz-credential-cache-race-v2-3-ddccd0831eba@gmail.com","threadId":"66742","inReplyTo":"20261104-fz-credential-cache-race-v2-0-ddccd0831eba@gmail.com","subject":"[PATCH v2 3/3] credential-cache--daemon: do not take over a running daemon's socket","fromName":"Farid Zakaria","fromEmail":"farid.m.zakaria@gmail.com","sentAt":"2026-11-04T19:53:16Z","receivedAt":"2026-11-04T19:53:16Z","isPatch":true,"sender":{"key":"farid.m.zakaria@gmail.com","avatar":"https://gravatar.com/avatar/13645be9e435a4ee3dd61831ce55d857aec3b1f26b0ec6bc700d170a9900d21c?d=mp&s=160"},"body":"When no daemon is running, two \"git credential-cache store\" run at the\nsame time each try to start one. unix_stream_listen() removes whatever\nis at the socket path before it binds, so the second daemon takes over\nthe socket of the first. The first daemon keeps running, but no client\ncan reach it:\n\n - the credential its client stored is lost;\n\n - \"git credential-cache exit\" does not stop it, and it keeps the\n   password in memory until it times out;\n\n - when it does exit, it removes the socket of the daemon that is\n   still running, losing that daemon's credentials too.\n\nFix this by creating the socket with unix_ss_create(), which the\nsimple-ipc server already uses. It takes a lock and binds only if no\nother server is listening on the socket. The lock is held only while\nbinding, so a stale lockfile left behind by a crashed daemon is\nunlikely to need manual cleanup.\n\nSigned-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>\n---\n builtin/credential-cache--daemon.c | 28 ++++++++++++++++++++--------\n t/t0301-credential-cache.sh        | 23 +++++++++++++++++++++++\n 2 files changed, 43 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c\nindex 9e6ed9c922..03d314f19f 100644\n--- a/builtin/credential-cache--daemon.c\n+++ b/builtin/credential-cache--daemon.c\n@@ -11,6 +11,7 @@\n #include \"tempfile.h\"\n #include \"credential.h\"\n #include \"unix-socket.h\"\n+#include \"unix-stream-server.h\"\n \n struct credential_cache_entry {\n \tstruct credential item;\n@@ -231,12 +232,25 @@ static int serve_cache_loop(int fd)\n static void serve_cache(const char *socket_path, int debug)\n {\n \tstruct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;\n-\tint fd;\n+\tstruct unix_ss_socket *server_socket;\n+\tstruct tempfile *socket_file;\n+\tint ret;\n \n-\tfd = unix_stream_listen(socket_path, &opts);\n-\tif (fd < 0)\n+\tret = unix_ss_create(socket_path, &opts, -1, &server_socket);\n+\tif (ret == -2) {\n+\t\t/*\n+\t\t * Another daemon is already listening on the socket. \"ok\"\n+\t\t * tells our client that a daemon is ready, after which the\n+\t\t * client connects to the socket and talks to that daemon.\n+\t\t */\n+\t\tprintf(\"ok\\n\");\n+\t\texit(0);\n+\t}\n+\tif (ret < 0)\n \t\tdie_errno(\"unable to bind to '%s'\", socket_path);\n \n+\tsocket_file = register_tempfile(socket_path);\n+\n \tprintf(\"ok\\n\");\n \tfclose(stdout);\n \tif (!debug) {\n@@ -244,10 +258,11 @@ static void serve_cache(const char *socket_path, int debug)\n \t\t\tdie_errno(\"unable to point stderr to /dev/null\");\n \t}\n \n-\twhile (serve_cache_loop(fd))\n+\twhile (serve_cache_loop(server_socket->fd_socket))\n \t\t; /* nothing */\n \n-\tclose(fd);\n+\tdelete_tempfile(&socket_file);\n+\tunix_ss_free(server_socket);\n }\n \n static const char permissions_advice[] = N_(\n@@ -303,7 +318,6 @@ int cmd_credential_cache_daemon(int argc,\n \t\t\t\tconst char *prefix,\n \t\t\t\tstruct repository *repo UNUSED)\n {\n-\tstruct tempfile *socket_file;\n \tconst char *socket_path;\n \tint ignore_sighup = 0;\n \tstatic const char *usage[] = {\n@@ -331,13 +345,11 @@ int cmd_credential_cache_daemon(int argc,\n \t\tdie(\"socket directory must be an absolute path\");\n \n \tinit_socket_directory(socket_path);\n-\tsocket_file = register_tempfile(socket_path);\n \n \tif (ignore_sighup)\n \t\tsignal(SIGHUP, SIG_IGN);\n \n \tserve_cache(socket_path, debug);\n-\tdelete_tempfile(&socket_file);\n \n \treturn 0;\n }\ndiff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh\nindex 6f7cfd9e33..8e07e55520 100755\n--- a/t/t0301-credential-cache.sh\n+++ b/t/t0301-credential-cache.sh\n@@ -51,6 +51,29 @@ test_expect_success 'socket defaults to ~/.cache/git/credential/socket' '\n \ttest_path_is_socket \"$HOME/.cache/git/credential/socket\"\n '\n \n+test_expect_success 'a second daemon does not take over the socket' '\n+\ttest_when_finished \"git credential-cache exit\" &&\n+\tcheck approve cache <<-\\EOF &&\n+\tprotocol=https\n+\thost=example.com\n+\tusername=store-user\n+\tpassword=store-pass\n+\tEOF\n+\tgit credential-cache--daemon \"$HOME/.cache/git/credential/socket\" >out &&\n+\techo ok >expect &&\n+\ttest_cmp expect out &&\n+\tcheck fill cache <<-\\EOF\n+\tprotocol=https\n+\thost=example.com\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=store-user\n+\tpassword=store-pass\n+\t--\n+\tEOF\n+'\n+\n XDG_CACHE_HOME=\"$HOME/xdg\"\n export XDG_CACHE_HOME\n # test behavior when XDG_CACHE_HOME is set\n\n-- \n2.55.0\n\n\n"}]}