Volume XXII, number 279Tuesday, October 6, 2026Latest message 34 minutes ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patchcredential/libsecret: load secrets explicitly

10 messages between Aug 4, 2026 and Sep 28, 2026, from Daniel Martí via GitGitGadget, Daniel Martí, Junio C Hamano, M Hickford.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Daniel Martí via GitGitGadgetAug 4, 2026, 22:40 UTC on lore
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <mvdan@mvdan.cc>

secret_service_search_sync() can return an item whose secret is not loaded, despite SECRET_SEARCH_LOAD_SECRETS being set: the search silently discards secret-loading failures, and the GNOME keyring daemon silently omits from its GetSecrets reply any item that is locked or that was deleted after the search matched it, e.g. by a concurrent "credential erase" from another git process.

secret_item_get_secret() then returns NULL, which we pass unchecked to secret_value_get_text() and secret_value_unref(), producing

    secret_value_get_text: assertion 'value' failed
    secret_value_unref: assertion 'value != NULL' failed
and losing the password even when the secret is still retrievable.

Drop SECRET_SEARCH_LOAD_SECRETS and instead load the secret of the one item we use with secret_item_load_secret_sync(), which does report errors. A secret the search would have silently dropped is now retrieved normally, and a genuinely inaccessible item produces a useful message instead of assertion spew, with git falling back to prompting either way. Merely guarding against NULL would avoid the assertions, but would forfeit a secret that is still available. The cost is unchanged: the search no longer batch-fetches the secrets of all matching items, and the explicit load fetches the one we use.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
---
    credential/libsecret: load secrets explicitly
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2372%2Fmvdan%2Flibsecret-null-secret-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2372/mvdan/libsecret-null-secret-v1
Pull-Request: https://github.com/git/git/pull/2372
 .../libsecret/git-credential-libsecret.c           | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)
Show changes to contrib/credential/libsecret/git-credential-libsecret.c +13 −1
diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c
index 941b2afd5e..6bbdf2bd45 100644
--- a/contrib/credential/libsecret/git-credential-libsecret.c
+++ b/contrib/credential/libsecret/git-credential-libsecret.c
@@ -126,7 +126,7 @@ static int keyring_get(struct credential *c)
 	items = secret_service_search_sync(service,
 					   &schema,
 					   attributes,
-					   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,
+					   SECRET_SEARCH_UNLOCK,
 					   NULL,
 					   &error);
 	g_hash_table_unref(attributes);
@@ -143,6 +143,18 @@ static int keyring_get(struct credential *c)
 		gchar **parts;
 
 		item = items->data;
+
+		/*
+		 * Load the secret explicitly rather than via
+		 * SECRET_SEARCH_LOAD_SECRETS, which silently discards load
+		 * failures and returns items whose secret is NULL.
+		 */
+		if (!secret_item_load_secret_sync(item, NULL, &error)) {
+			g_critical("could not load secret: %s", error->message);
+			g_error_free(error);
+			g_list_free_full(items, g_object_unref);
+			return EXIT_FAILURE;
+		}
 		secret = secret_item_get_secret(item);
 		attributes = secret_item_get_attributes(item);
 

base-commit: 5b2471720c93ee30e5764a19f3d3b3ae9ec9712a
-- 
gitgitgadget
Daniel MartíAug 20, 2026, 15:00 UTC in reply to Daniel Martí via GitGitGadget on lore

Re: [PATCH] credential/libsecret: load secrets explicitly

Gentle nudge on this, anything I can do to assist in getting it reviewed?
I still run into the error a few times per week :)
On 8/4/26 11:40 PM, Daniel Martí via GitGitGadget wrote:
Show 73 quoted lines
> From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <mvdan@mvdan.cc>
>
> secret_service_search_sync() can return an item whose secret is not
> loaded, despite SECRET_SEARCH_LOAD_SECRETS being set: the search
> silently discards secret-loading failures, and the GNOME keyring
> daemon silently omits from its GetSecrets reply any item that is
> locked or that was deleted after the search matched it, e.g. by a
> concurrent "credential erase" from another git process.
>
> secret_item_get_secret() then returns NULL, which we pass unchecked
> to secret_value_get_text() and secret_value_unref(), producing
>
>      secret_value_get_text: assertion 'value' failed
>      secret_value_unref: assertion 'value != NULL' failed
>
> and losing the password even when the secret is still retrievable.
>
> Drop SECRET_SEARCH_LOAD_SECRETS and instead load the secret of the
> one item we use with secret_item_load_secret_sync(), which does
> report errors. A secret the search would have silently dropped is
> now retrieved normally, and a genuinely inaccessible item produces
> a useful message instead of assertion spew, with git falling back
> to prompting either way. Merely guarding against NULL would avoid
> the assertions, but would forfeit a secret that is still available.
> The cost is unchanged: the search no longer batch-fetches the
> secrets of all matching items, and the explicit load fetches the
> one we use.
>
> Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
> ---
>      credential/libsecret: load secrets explicitly
>
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2372%2Fmvdan%2Flibsecret-null-secret-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2372/mvdan/libsecret-null-secret-v1
> Pull-Request: https://github.com/git/git/pull/2372
>
>   .../libsecret/git-credential-libsecret.c           | 14 +++++++++++++-
>   1 file changed, 13 insertions(+), 1 deletion(-)
>
> diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c
> index 941b2afd5e..6bbdf2bd45 100644
> --- a/contrib/credential/libsecret/git-credential-libsecret.c
> +++ b/contrib/credential/libsecret/git-credential-libsecret.c
> @@ -126,7 +126,7 @@ static int keyring_get(struct credential *c)
>   	items = secret_service_search_sync(service,
>   					   &schema,
>   					   attributes,
> -					   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,
> +					   SECRET_SEARCH_UNLOCK,
>   					   NULL,
>   					   &error);
>   	g_hash_table_unref(attributes);
> @@ -143,6 +143,18 @@ static int keyring_get(struct credential *c)
>   		gchar **parts;
>   
>   		item = items->data;
> +
> +		/*
> +		 * Load the secret explicitly rather than via
> +		 * SECRET_SEARCH_LOAD_SECRETS, which silently discards load
> +		 * failures and returns items whose secret is NULL.
> +		 */
> +		if (!secret_item_load_secret_sync(item, NULL, &error)) {
> +			g_critical("could not load secret: %s", error->message);
> +			g_error_free(error);
> +			g_list_free_full(items, g_object_unref);
> +			return EXIT_FAILURE;
> +		}
>   		secret = secret_item_get_secret(item);
>   		attributes = secret_item_get_attributes(item);
>   
>
> base-commit: 5b2471720c93ee30e5764a19f3d3b3ae9ec9712a
Junio C HamanoAug 20, 2026, 19:05 UTC in reply to Daniel Martí on lore

Re: [PATCH] credential/libsecret: load secrets explicitly

Daniel Martí <mvdan@mvdan.cc> writes:
Show 16 quoted lines
> Gentle nudge on this, anything I can do to assist in getting it reviewed?
>
> I still run into the error a few times per week :)
>
> On 8/4/26 11:40 PM, Daniel Martí via GitGitGadget wrote:
>> From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <mvdan@mvdan.cc>
>>
>> secret_service_search_sync() can return an item whose secret is not
>> loaded, despite SECRET_SEARCH_LOAD_SECRETS being set: the search
>> silently discards secret-loading failures, and the GNOME keyring
>> daemon silently omits from its GetSecrets reply any item that is
>> locked or that was deleted after the search matched it, e.g. by a
>> concurrent "credential erase" from another git process.
>>
>> secret_item_get_secret() then returns NULL, which we pass unchecked
>> to secret_value_get_text() and secret_value_unref(), producing

I do not program Gnome so I am not exactly qualified to review this, but anyway.

The above makes it sound like we can just request with secret_service_search_sync() exactly the same way as before (i.e., with LOAD_SECRETS), and then check with secret_item_get_secret() to see if it has secret value in it. The problem with the current code is that we do not validate what that secrete value is (iow, we do not say "ah, NULL, we should not assume we do have secret already obtained here").

So does the first hunk to drop _LOAD_SECRETS really needed? Rather, would it be more straight-forward to do

	item = items->data;
	secret = secret_item_get_secret(item);
	if (!secret &&
            !secret_item_load_secret_sync(item, NULL, &error)) {
		... your error handling here ...
		return EXIT_FAILURE;
	} 
        if (!secret)
		secret = secret_item_get_secret(item);

instead? I am assuming that it is rare (like, only a few times a week) to race with other activities that unloads the secret and most of the time the first secret_service_search_sync() gets everything needed in a single call.

I am also assuming that this is a race condition that is not very easy to reliably reproduce in the test, so I wouldn't expect it to come with a test to ensure that the fix will not regress in the future (in other words, lack of patch to t/ directory is fine).

Thanks.
Show 62 quoted lines
>>
>>      secret_value_get_text: assertion 'value' failed
>>      secret_value_unref: assertion 'value != NULL' failed
>>
>> and losing the password even when the secret is still retrievable.
>>
>> Drop SECRET_SEARCH_LOAD_SECRETS and instead load the secret of the
>> one item we use with secret_item_load_secret_sync(), which does
>> report errors. A secret the search would have silently dropped is
>> now retrieved normally, and a genuinely inaccessible item produces
>> a useful message instead of assertion spew, with git falling back
>> to prompting either way. Merely guarding against NULL would avoid
>> the assertions, but would forfeit a secret that is still available.
>> The cost is unchanged: the search no longer batch-fetches the
>> secrets of all matching items, and the explicit load fetches the
>> one we use.
>>
>> Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
>> ---
>>      credential/libsecret: load secrets explicitly
>>
>> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2372%2Fmvdan%2Flibsecret-null-secret-v1
>> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2372/mvdan/libsecret-null-secret-v1
>> Pull-Request: https://github.com/git/git/pull/2372
>>
>>   .../libsecret/git-credential-libsecret.c           | 14 +++++++++++++-
>>   1 file changed, 13 insertions(+), 1 deletion(-)
>>
>> diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c
>> index 941b2afd5e..6bbdf2bd45 100644
>> --- a/contrib/credential/libsecret/git-credential-libsecret.c
>> +++ b/contrib/credential/libsecret/git-credential-libsecret.c
>> @@ -126,7 +126,7 @@ static int keyring_get(struct credential *c)
>>   	items = secret_service_search_sync(service,
>>   					   &schema,
>>   					   attributes,
>> -					   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,
>> +					   SECRET_SEARCH_UNLOCK,
>>   					   NULL,
>>   					   &error);
>>   	g_hash_table_unref(attributes);
>> @@ -143,6 +143,18 @@ static int keyring_get(struct credential *c)
>>   		gchar **parts;
>>   
>>   		item = items->data;
>> +
>> +		/*
>> +		 * Load the secret explicitly rather than via
>> +		 * SECRET_SEARCH_LOAD_SECRETS, which silently discards load
>> +		 * failures and returns items whose secret is NULL.
>> +		 */
>> +		if (!secret_item_load_secret_sync(item, NULL, &error)) {
>> +			g_critical("could not load secret: %s", error->message);
>> +			g_error_free(error);
>> +			g_list_free_full(items, g_object_unref);
>> +			return EXIT_FAILURE;
>> +		}
>>   		secret = secret_item_get_secret(item);
>>   		attributes = secret_item_get_attributes(item);
>>   
>>
>> base-commit: 5b2471720c93ee30e5764a19f3d3b3ae9ec9712a
Daniel MartíAug 22, 2026, 20:47 UTC in reply to Junio C Hamano on lore

Re: [PATCH] credential/libsecret: load secrets explicitly

Hi Junio, thanks very much for your review. I also do not program Gnome, so this is just based on my best effort research :)

Junio C Hamano <gitster@pobox.com> writes:
 > The above makes it sound like we can just request with
 > secret_service_search_sync() exactly the same way as before (i.e.,
 > with LOAD_SECRETS), and then check with secret_item_get_secret()
 > to see if it has secret value in it.  The problem with the current
 > code is that we do not validate what that secrete value is (iow, we
 > do not say "ah, NULL, we should not assume we do have secret already
 > obtained here").
 >
 > So does the first hunk to drop _LOAD_SECRETS really needed?
Not strictly; your version would avoid the assertion failures too.

I dropped the flag because it isn't the single call it appears to be: libsecret implements SECRET_SEARCH_LOAD_SECRETS as the SearchItems D-Bus call followed by a separate GetSecrets call covering the matched items, with any error from that second step discarded. It also skips locked items on the client side without even asking the daemon, so SECRET_SEARCH_UNLOCK having silently failed is one way to end up with a NULL secret. The patch does SearchItems plus one GetSecret for the one item we use, so the common case is the same two round trips either way; it is your fallback version that would add a third in the rare case. That is what the "cost is unchanged" paragraph in the commit message is trying to say.

Keeping the flag with a fallback load would also mean the explicit load only runs during the rare race, so that path would go essentially unexercised, and load errors on the common path would still be silently discarded. With the unconditional load there is one code path, taken on every run, and any failure produces a real error message.

 > I am also assuming that this is a race condition that is not very
 > easy to reliably reproduce in the test, so I wouldn't expect it to
 > come with a test to ensure that the fix will not regress in the
 > future (in other words, lack of patch to t/ directory is fine).

Right, it needs a concurrently-racing process (or a locked keyring) to trigger, so I don't see a way to test it reliably.

Thanks!
Daniel MartíSep 21, 2026, 21:54 UTC in reply to Daniel Martí on lore

Re: [PATCH] credential/libsecret: load secrets explicitly

Friendly nudge :) Happy to look into any tweaks or questions.
On 8/22/26 9:47 PM, Daniel Martí wrote:
Show 44 quoted lines
> Hi Junio, thanks very much for your review. I also do not program Gnome,
> so this is just based on my best effort research :)
>
> Junio C Hamano <gitster@pobox.com> writes:
> > The above makes it sound like we can just request with
> > secret_service_search_sync() exactly the same way as before (i.e.,
> > with LOAD_SECRETS), and then check with secret_item_get_secret()
> > to see if it has secret value in it.  The problem with the current
> > code is that we do not validate what that secrete value is (iow, we
> > do not say "ah, NULL, we should not assume we do have secret already
> > obtained here").
> >
> > So does the first hunk to drop _LOAD_SECRETS really needed?
>
> Not strictly; your version would avoid the assertion failures too.
>
> I dropped the flag because it isn't the single call it appears to be:
> libsecret implements SECRET_SEARCH_LOAD_SECRETS as the SearchItems
> D-Bus call followed by a separate GetSecrets call covering the matched
> items, with any error from that second step discarded. It also skips
> locked items on the client side without even asking the daemon, so
> SECRET_SEARCH_UNLOCK having silently failed is one way to end up with
> a NULL secret. The patch does SearchItems plus one GetSecret for the
> one item we use, so the common case is the same two round trips either
> way; it is your fallback version that would add a third in the rare
> case. That is what the "cost is unchanged" paragraph in the commit
> message is trying to say.
>
> Keeping the flag with a fallback load would also mean the explicit
> load only runs during the rare race, so that path would go essentially
> unexercised, and load errors on the common path would still be
> silently discarded. With the unconditional load there is one code
> path, taken on every run, and any failure produces a real error
> message.
>
> > I am also assuming that this is a race condition that is not very
> > easy to reliably reproduce in the test, so I wouldn't expect it to
> > come with a test to ensure that the fix will not regress in the
> > future (in other words, lack of patch to t/ directory is fine).
>
> Right, it needs a concurrently-racing process (or a locked keyring)
> to trigger, so I don't see a way to test it reliably.
>
> Thanks!
Junio C HamanoSep 21, 2026, 22:52 UTC in reply to Daniel Martí on lore

Re: [PATCH] credential/libsecret: load secrets explicitly

Daniel Martí <mvdan@mvdan.cc> writes:
> Friendly nudge :) Happy to look into any tweaks or questions.

We have had a review exchange. A reviewer had to ask questions on the points where the code and the proposed log message weren't clear. The ball is now in the author's court to improve the patch to the point that the next reviewer who did not see the first exchange would not have to ask the same questions.

Thanks for working on this topic.
M HickfordSep 24, 2026, 07:00 UTC in reply to Daniel Martí via GitGitGadget on lore

Re: [PATCH] credential/libsecret: load secrets explicitly

Thanks for sharing

On Tue, 4 Aug 2026 at 23:40, Daniel Martí via GitGitGadget <gitgitgadget@gmail.com> wrote:

Show 30 quoted lines
>
> From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <mvdan@mvdan.cc>
>
> secret_service_search_sync() can return an item whose secret is not
> loaded, despite SECRET_SEARCH_LOAD_SECRETS being set: the search
> silently discards secret-loading failures, and the GNOME keyring
> daemon silently omits from its GetSecrets reply any item that is
> locked or that was deleted after the search matched it, e.g. by a
> concurrent "credential erase" from another git process.
>
> secret_item_get_secret() then returns NULL, which we pass unchecked
> to secret_value_get_text() and secret_value_unref(), producing
>
>     secret_value_get_text: assertion 'value' failed
>     secret_value_unref: assertion 'value != NULL' failed
>
> and losing the password even when the secret is still retrievable.
>
> Drop SECRET_SEARCH_LOAD_SECRETS and instead load the secret of the
> one item we use with secret_item_load_secret_sync(), which does
> report errors. A secret the search would have silently dropped is
> now retrieved normally, and a genuinely inaccessible item produces
> a useful message instead of assertion spew, with git falling back
> to prompting either way. Merely guarding against NULL would avoid
> the assertions, but would forfeit a secret that is still available.
> The cost is unchanged: the search no longer batch-fetches the
> secrets of all matching items, and the explicit load fetches the
> one we use.
>
> Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
Thanks for explaining the motivation.
Is this an upstream bug in libsecret?

The libsecret docs for SECRET_SEARCH_LOAD_SECRETS are unfortunately truncated https://gnome.pages.gitlab.gnome.org/libsecret/method.Service.search_sync.html

> If SECRET_SEARCH_LOAD_SECRETS is set in flags, then the items’ secret values will be loaded for any unlocked items. Loaded item secret values are available via secret_item_get_secret(). If the load of a secret values fail, then the [mystery consequence]
Show 46 quoted lines
> ---
>     credential/libsecret: load secrets explicitly
>
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2372%2Fmvdan%2Flibsecret-null-secret-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2372/mvdan/libsecret-null-secret-v1
> Pull-Request: https://github.com/git/git/pull/2372
>
>  .../libsecret/git-credential-libsecret.c           | 14 +++++++++++++-
>  1 file changed, 13 insertions(+), 1 deletion(-)
>
> diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c
> index 941b2afd5e..6bbdf2bd45 100644
> --- a/contrib/credential/libsecret/git-credential-libsecret.c
> +++ b/contrib/credential/libsecret/git-credential-libsecret.c
> @@ -126,7 +126,7 @@ static int keyring_get(struct credential *c)
>         items = secret_service_search_sync(service,
>                                            &schema,
>                                            attributes,
> -                                          SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,
> +                                          SECRET_SEARCH_UNLOCK,
>                                            NULL,
>                                            &error);
>         g_hash_table_unref(attributes);
> @@ -143,6 +143,18 @@ static int keyring_get(struct credential *c)
>                 gchar **parts;
>
>                 item = items->data;
> +
> +               /*
> +                * Load the secret explicitly rather than via
> +                * SECRET_SEARCH_LOAD_SECRETS, which silently discards load
> +                * failures and returns items whose secret is NULL.
> +                */
> +               if (!secret_item_load_secret_sync(item, NULL, &error)) {
> +                       g_critical("could not load secret: %s", error->message);
> +                       g_error_free(error);
> +                       g_list_free_full(items, g_object_unref);
> +                       return EXIT_FAILURE;
> +               }
>                 secret = secret_item_get_secret(item);
>                 attributes = secret_item_get_attributes(item);
>
>
> base-commit: 5b2471720c93ee30e5764a19f3d3b3ae9ec9712a
> --
> gitgitgadget
Daniel MartíSep 27, 2026, 22:45 UTC in reply to M Hickford on lore

Re: [PATCH] credential/libsecret: load secrets explicitly

On 9/24/26 8:00 AM, M Hickford wrote:
> Is this an upstream bug in libsecret?
>
> The libsecret docs for SECRET_SEARCH_LOAD_SECRETS  are unfortunately
> truncated https://gnome.pages.gitlab.gnome.org/libsecret/method.Service.search_sync.html

Partly. The truncated sentence is a docs bug, which I've sent a fix for: https://gitlab.gnome.org/GNOME/libsecret/-/merge_requests/182

The behavior itself looks intentional, though. The search does not load secrets of locked items, and just like a failed unlock, a failed load does not fail the search; secret_item_get_secret() is documented to return NULL for a locked or unloaded item. The daemon side is deliberate too: gnome-keyring's GetSecrets skips items which are locked or no longer exist, whereas GetSecret on a single item returns an error.

So git needs to handle a NULL secret either way, including with every libsecret release out there. libsecret's own secret-tool also loads each secret explicitly after searching, which is what this patch does.

I'll send a v2 with a reworded commit message shortly.
Thanks!
Daniel Martí via GitGitGadgetSep 27, 2026, 22:46 UTC in reply to Daniel Martí via GitGitGadget on lore

[PATCH v2] credential/libsecret: load secrets explicitly

From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <mvdan@mvdan.cc>

keyring_get() searches with SECRET_SEARCH_LOAD_SECRETS, then passes secret_item_get_secret() of the first match unchecked to secret_value_get_text() and secret_value_unref(). As libsecret documents, that secret can be NULL: the search does not load secrets of locked items, such as when SECRET_SEARCH_UNLOCK fails to unlock them, and it ignores errors from loading secrets. The GNOME keyring daemon also silently leaves out of its reply any item which is locked or which was deleted after the search matched it, e.g. by a concurrent "credential erase" from another git process. We then print

    secret_value_get_text: assertion 'value' failed
    secret_value_unref: assertion 'value != NULL' failed
before git falls back to prompting for the password.

We could keep the flag and load the secret explicitly only when it is NULL, but SECRET_SEARCH_LOAD_SECRETS is not part of the search call: libsecret implements it as a separate GetSecrets D-Bus call after SearchItems. Drop the flag and instead always load the one secret we use with secret_item_load_secret_sync(), which reports errors. This takes as many D-Bus calls as before, and leaves a single code path that runs every time, rather than a fallback that only runs in a rare race. libsecret's own secret-tool also loads each secret explicitly after searching.

An inaccessible item now produces a useful error message instead of the assertion failures, and git still falls back to prompting. The race needs a concurrent process or a locked keyring to trigger, so there is no test.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
---
    credential/libsecret: load secrets explicitly
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2372%2Fmvdan%2Flibsecret-null-secret-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2372/mvdan/libsecret-null-secret-v2
Pull-Request: https://github.com/git/git/pull/2372
Range-diff vs v1:
 1:  89d3eee91f ! 1:  b9ddf13fa1 credential/libsecret: load secrets explicitly
     @@ Metadata
       ## Commit message ##
          credential/libsecret: load secrets explicitly
      
     -    secret_service_search_sync() can return an item whose secret is not
     -    loaded, despite SECRET_SEARCH_LOAD_SECRETS being set: the search
     -    silently discards secret-loading failures, and the GNOME keyring
     -    daemon silently omits from its GetSecrets reply any item that is
     -    locked or that was deleted after the search matched it, e.g. by a
     -    concurrent "credential erase" from another git process.
     -
     -    secret_item_get_secret() then returns NULL, which we pass unchecked
     -    to secret_value_get_text() and secret_value_unref(), producing
     +    keyring_get() searches with SECRET_SEARCH_LOAD_SECRETS, then passes
     +    secret_item_get_secret() of the first match unchecked to
     +    secret_value_get_text() and secret_value_unref(). As libsecret
     +    documents, that secret can be NULL: the search does not load secrets
     +    of locked items, such as when SECRET_SEARCH_UNLOCK fails to unlock
     +    them, and it ignores errors from loading secrets. The GNOME keyring
     +    daemon also silently leaves out of its reply any item which is locked
     +    or which was deleted after the search matched it, e.g. by a concurrent
     +    "credential erase" from another git process. We then print
      
              secret_value_get_text: assertion 'value' failed
              secret_value_unref: assertion 'value != NULL' failed
      
     -    and losing the password even when the secret is still retrievable.
     +    before git falls back to prompting for the password.
     +
     +    We could keep the flag and load the secret explicitly only when it is
     +    NULL, but SECRET_SEARCH_LOAD_SECRETS is not part of the search call:
     +    libsecret implements it as a separate GetSecrets D-Bus call after
     +    SearchItems. Drop the flag and instead always load the one secret we
     +    use with secret_item_load_secret_sync(), which reports errors. This
     +    takes as many D-Bus calls as before, and leaves a single code path
     +    that runs every time, rather than a fallback that only runs in a rare
     +    race. libsecret's own secret-tool also loads each secret explicitly
     +    after searching.
      
     -    Drop SECRET_SEARCH_LOAD_SECRETS and instead load the secret of the
     -    one item we use with secret_item_load_secret_sync(), which does
     -    report errors. A secret the search would have silently dropped is
     -    now retrieved normally, and a genuinely inaccessible item produces
     -    a useful message instead of assertion spew, with git falling back
     -    to prompting either way. Merely guarding against NULL would avoid
     -    the assertions, but would forfeit a secret that is still available.
     -    The cost is unchanged: the search no longer batch-fetches the
     -    secrets of all matching items, and the explicit load fetches the
     -    one we use.
     +    An inaccessible item now produces a useful error message instead of
     +    the assertion failures, and git still falls back to prompting. The
     +    race needs a concurrent process or a locked keyring to trigger, so
     +    there is no test.
      
          Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
      
     @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(
      +
      +		/*
      +		 * Load the secret explicitly rather than via
     -+		 * SECRET_SEARCH_LOAD_SECRETS, which silently discards load
     -+		 * failures and returns items whose secret is NULL.
     ++		 * SECRET_SEARCH_LOAD_SECRETS, which skips locked items and
     ++		 * ignores load failures, leaving the secret NULL.
      +		 */
      +		if (!secret_item_load_secret_sync(item, NULL, &error)) {
      +			g_critical("could not load secret: %s", error->message);
 .../libsecret/git-credential-libsecret.c           | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)
Show changes to contrib/credential/libsecret/git-credential-libsecret.c +13 −1
diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c
index 941b2afd5e..ad4f60e4d7 100644
--- a/contrib/credential/libsecret/git-credential-libsecret.c
+++ b/contrib/credential/libsecret/git-credential-libsecret.c
@@ -126,7 +126,7 @@ static int keyring_get(struct credential *c)
 	items = secret_service_search_sync(service,
 					   &schema,
 					   attributes,
-					   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,
+					   SECRET_SEARCH_UNLOCK,
 					   NULL,
 					   &error);
 	g_hash_table_unref(attributes);
@@ -143,6 +143,18 @@ static int keyring_get(struct credential *c)
 		gchar **parts;
 
 		item = items->data;
+
+		/*
+		 * Load the secret explicitly rather than via
+		 * SECRET_SEARCH_LOAD_SECRETS, which skips locked items and
+		 * ignores load failures, leaving the secret NULL.
+		 */
+		if (!secret_item_load_secret_sync(item, NULL, &error)) {
+			g_critical("could not load secret: %s", error->message);
+			g_error_free(error);
+			g_list_free_full(items, g_object_unref);
+			return EXIT_FAILURE;
+		}
 		secret = secret_item_get_secret(item);
 		attributes = secret_item_get_attributes(item);
 

base-commit: 34f06850c16c7f7ac822b1adc71354f11b0f2ca3
-- 
gitgitgadget
Daniel MartíSep 28, 2026, 07:37 UTC in reply to Daniel Martí via GitGitGadget on lore

Re: [PATCH v2] credential/libsecret: load secrets explicitly

I didn't add the "changes since v1" text to the right section of the PR, so the bot didn't pick it up correctly. Adding it here now...

Changes since v1:
- Reworded the commit message to answer Junio's questions: why the 
explicit load replaces SECRET_SEARCH_LOAD_SECRETS rather than serving as 
a fallback, what it costs, and why there is no test.
- Dropped the claim that a secret dropped by the search is now 
retrieved; a deleted or still-locked item fails the explicit load too, 
only with a proper error message.
- Updated the code comment to say what libsecret actually does.
On 9/27/26 11:46 PM, Daniel Martí via GitGitGadget wrote:
Show 149 quoted lines
> From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <mvdan@mvdan.cc>
>
> keyring_get() searches with SECRET_SEARCH_LOAD_SECRETS, then passes
> secret_item_get_secret() of the first match unchecked to
> secret_value_get_text() and secret_value_unref(). As libsecret
> documents, that secret can be NULL: the search does not load secrets
> of locked items, such as when SECRET_SEARCH_UNLOCK fails to unlock
> them, and it ignores errors from loading secrets. The GNOME keyring
> daemon also silently leaves out of its reply any item which is locked
> or which was deleted after the search matched it, e.g. by a concurrent
> "credential erase" from another git process. We then print
>
>      secret_value_get_text: assertion 'value' failed
>      secret_value_unref: assertion 'value != NULL' failed
>
> before git falls back to prompting for the password.
>
> We could keep the flag and load the secret explicitly only when it is
> NULL, but SECRET_SEARCH_LOAD_SECRETS is not part of the search call:
> libsecret implements it as a separate GetSecrets D-Bus call after
> SearchItems. Drop the flag and instead always load the one secret we
> use with secret_item_load_secret_sync(), which reports errors. This
> takes as many D-Bus calls as before, and leaves a single code path
> that runs every time, rather than a fallback that only runs in a rare
> race. libsecret's own secret-tool also loads each secret explicitly
> after searching.
>
> An inaccessible item now produces a useful error message instead of
> the assertion failures, and git still falls back to prompting. The
> race needs a concurrent process or a locked keyring to trigger, so
> there is no test.
>
> Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
> ---
>      credential/libsecret: load secrets explicitly
>
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2372%2Fmvdan%2Flibsecret-null-secret-v2
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2372/mvdan/libsecret-null-secret-v2
> Pull-Request: https://github.com/git/git/pull/2372
>
> Range-diff vs v1:
>
>   1:  89d3eee91f ! 1:  b9ddf13fa1 credential/libsecret: load secrets explicitly
>       @@ Metadata
>         ## Commit message ##
>            credential/libsecret: load secrets explicitly
>        
>       -    secret_service_search_sync() can return an item whose secret is not
>       -    loaded, despite SECRET_SEARCH_LOAD_SECRETS being set: the search
>       -    silently discards secret-loading failures, and the GNOME keyring
>       -    daemon silently omits from its GetSecrets reply any item that is
>       -    locked or that was deleted after the search matched it, e.g. by a
>       -    concurrent "credential erase" from another git process.
>       -
>       -    secret_item_get_secret() then returns NULL, which we pass unchecked
>       -    to secret_value_get_text() and secret_value_unref(), producing
>       +    keyring_get() searches with SECRET_SEARCH_LOAD_SECRETS, then passes
>       +    secret_item_get_secret() of the first match unchecked to
>       +    secret_value_get_text() and secret_value_unref(). As libsecret
>       +    documents, that secret can be NULL: the search does not load secrets
>       +    of locked items, such as when SECRET_SEARCH_UNLOCK fails to unlock
>       +    them, and it ignores errors from loading secrets. The GNOME keyring
>       +    daemon also silently leaves out of its reply any item which is locked
>       +    or which was deleted after the search matched it, e.g. by a concurrent
>       +    "credential erase" from another git process. We then print
>        
>                secret_value_get_text: assertion 'value' failed
>                secret_value_unref: assertion 'value != NULL' failed
>        
>       -    and losing the password even when the secret is still retrievable.
>       +    before git falls back to prompting for the password.
>       +
>       +    We could keep the flag and load the secret explicitly only when it is
>       +    NULL, but SECRET_SEARCH_LOAD_SECRETS is not part of the search call:
>       +    libsecret implements it as a separate GetSecrets D-Bus call after
>       +    SearchItems. Drop the flag and instead always load the one secret we
>       +    use with secret_item_load_secret_sync(), which reports errors. This
>       +    takes as many D-Bus calls as before, and leaves a single code path
>       +    that runs every time, rather than a fallback that only runs in a rare
>       +    race. libsecret's own secret-tool also loads each secret explicitly
>       +    after searching.
>        
>       -    Drop SECRET_SEARCH_LOAD_SECRETS and instead load the secret of the
>       -    one item we use with secret_item_load_secret_sync(), which does
>       -    report errors. A secret the search would have silently dropped is
>       -    now retrieved normally, and a genuinely inaccessible item produces
>       -    a useful message instead of assertion spew, with git falling back
>       -    to prompting either way. Merely guarding against NULL would avoid
>       -    the assertions, but would forfeit a secret that is still available.
>       -    The cost is unchanged: the search no longer batch-fetches the
>       -    secrets of all matching items, and the explicit load fetches the
>       -    one we use.
>       +    An inaccessible item now produces a useful error message instead of
>       +    the assertion failures, and git still falls back to prompting. The
>       +    race needs a concurrent process or a locked keyring to trigger, so
>       +    there is no test.
>        
>            Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
>        
>       @@ contrib/credential/libsecret/git-credential-libsecret.c: static int keyring_get(
>        +
>        +		/*
>        +		 * Load the secret explicitly rather than via
>       -+		 * SECRET_SEARCH_LOAD_SECRETS, which silently discards load
>       -+		 * failures and returns items whose secret is NULL.
>       ++		 * SECRET_SEARCH_LOAD_SECRETS, which skips locked items and
>       ++		 * ignores load failures, leaving the secret NULL.
>        +		 */
>        +		if (!secret_item_load_secret_sync(item, NULL, &error)) {
>        +			g_critical("could not load secret: %s", error->message);
>
>
>   .../libsecret/git-credential-libsecret.c           | 14 +++++++++++++-
>   1 file changed, 13 insertions(+), 1 deletion(-)
>
> diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c
> index 941b2afd5e..ad4f60e4d7 100644
> --- a/contrib/credential/libsecret/git-credential-libsecret.c
> +++ b/contrib/credential/libsecret/git-credential-libsecret.c
> @@ -126,7 +126,7 @@ static int keyring_get(struct credential *c)
>   	items = secret_service_search_sync(service,
>   					   &schema,
>   					   attributes,
> -					   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,
> +					   SECRET_SEARCH_UNLOCK,
>   					   NULL,
>   					   &error);
>   	g_hash_table_unref(attributes);
> @@ -143,6 +143,18 @@ static int keyring_get(struct credential *c)
>   		gchar **parts;
>   
>   		item = items->data;
> +
> +		/*
> +		 * Load the secret explicitly rather than via
> +		 * SECRET_SEARCH_LOAD_SECRETS, which skips locked items and
> +		 * ignores load failures, leaving the secret NULL.
> +		 */
> +		if (!secret_item_load_secret_sync(item, NULL, &error)) {
> +			g_critical("could not load secret: %s", error->message);
> +			g_error_free(error);
> +			g_list_free_full(items, g_object_unref);
> +			return EXIT_FAILURE;
> +		}
>   		secret = secret_item_get_secret(item);
>   		attributes = secret_item_get_attributes(item);
>   
>
> base-commit: 34f06850c16c7f7ac822b1adc71354f11b0f2ca3

Back to recent threads