# [PATCH] credential/libsecret: load secrets explicitly

10 messages from 2026-08-04 to 2026-09-28. Participants: Daniel Martí via GitGitGadget, Daniel Martí, Junio C Hamano, M Hickford.
Thread: https://gitlist.dev/t/66117

## Daniel Martí via GitGitGadget, 2026-08-04 22:40

Subject: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <pull.2372.git.git.1785883217733.gitgitgadget@gmail.com>

```
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
-- 
gitgitgadget

```

## Daniel Martí, 2026-08-20 15:00

Subject: Re: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <331e94ec-c07a-4d56-938d-d12c9a0cc5c9@mvdan.cc>
In-Reply-To: <pull.2372.git.git.1785883217733.gitgitgadget@gmail.com>

```
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
>
>      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 Hamano, 2026-08-20 19:05

Subject: Re: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <xmqq33w8sjcg.fsf@gitster.g>
In-Reply-To: <331e94ec-c07a-4d56-938d-d12c9a0cc5c9@mvdan.cc>

```
Daniel Martí <mvdan@mvdan.cc> writes:

> 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.


>>
>>      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í, 2026-08-22 20:47

Subject: Re: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <a60afe54-7059-4827-8c3b-dfe01a0cde9c@mvdan.cc>
In-Reply-To: <xmqq33w8sjcg.fsf@gitster.g>

```
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í, 2026-09-21 21:54

Subject: Re: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <c3f51472-8dfa-42df-aa8b-b0f5bfd0aafe@mvdan.cc>
In-Reply-To: <a60afe54-7059-4827-8c3b-dfe01a0cde9c@mvdan.cc>

```
Friendly nudge :) Happy to look into any tweaks or questions.

On 8/22/26 9:47 PM, Daniel Martí wrote:
> 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 Hamano, 2026-09-21 22:52

Subject: Re: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <xmqqv77yjjys.fsf@gitster.g>
In-Reply-To: <c3f51472-8dfa-42df-aa8b-b0f5bfd0aafe@mvdan.cc>

```
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 Hickford, 2026-09-24 07:00

Subject: Re: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <CAGJzqs=sUA7vGDwadL9h-dcuPAsQvhAjiirZhA5=_fyqH1QXuA@mail.gmail.com>
In-Reply-To: <pull.2372.git.git.1785883217733.gitgitgadget@gmail.com>

```
Thanks for sharing

On Tue, 4 Aug 2026 at 23:40, Daniel Martí via GitGitGadget
<gitgitgadget@gmail.com> 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
>
>     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]


> ---
>     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í, 2026-09-27 22:45

Subject: Re: [PATCH] credential/libsecret: load secrets explicitly
Message-ID: <3cae7bd6-33fa-4695-bf4e-9f473ac98042@mvdan.cc>
In-Reply-To: <CAGJzqs=sUA7vGDwadL9h-dcuPAsQvhAjiirZhA5=_fyqH1QXuA@mail.gmail.com>

```
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 GitGitGadget, 2026-09-27 22:46

Subject: [PATCH v2] credential/libsecret: load secrets explicitly
Message-ID: <pull.2372.v2.git.git.1790549181518.gitgitgadget@gmail.com>
In-Reply-To: <pull.2372.git.git.1785883217733.gitgitgadget@gmail.com>

```
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
-- 
gitgitgadget

```

## Daniel Martí, 2026-09-28 07:37

Subject: Re: [PATCH v2] credential/libsecret: load secrets explicitly
Message-ID: <f73a8b6c-37cd-4804-8587-3df4f0999f0a@mvdan.cc>
In-Reply-To: <pull.2372.v2.git.git.1790549181518.gitgitgadget@gmail.com>

```
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:
> 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

```
