credential/libsecret: load secrets explicitly
PATCH 1/1 credential/libsecret: load secrets explicitly
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
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.
---
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(-)
contrib/credential/libsecret/git-credential-libsecret.c +13 -1
@@ -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
Signature · 1 line
gitgitgadget
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 :)
Quoted 73 lines · Daniel Martí via GitGitGadget
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
secret_service_search_sync()can return an item whose secret is not loaded, despiteSECRET_SEARCH_LOAD_SECRETSbeing 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 tosecret_value_get_text()andsecret_value_unref(), producing
secret_value_get_text: assertion 'value' failed
secret_value_unref: assertion 'value != NULL' failedand losing the password even when the secret is still retrievable.
Drop
SECRET_SEARCH_LOAD_SECRETSand instead load the secret of the one item we use withsecret_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í <[email protected]>---
credential/libsecret: load secrets explicitlyPublished-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(-)contrib/credential/libsecret/git-credential-libsecret.c +13
-1@@ -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
Quoted 16 lines · Daniel Martí
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 :)
Quoted 11 lines · Daniel Martí via GitGitGadget
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
secret_service_search_sync()can return an item whose secret is not loaded, despiteSECRET_SEARCH_LOAD_SECRETSbeing 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 tosecret_value_get_text()andsecret_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.
Quoted 62 lines
Quoted 62 lines
secret_value_get_text: assertion 'value' failed
secret_value_unref: assertion 'value != NULL' failedand losing the password even when the secret is still retrievable.
Drop
SECRET_SEARCH_LOAD_SECRETSand instead load the secret of the one item we use withsecret_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í <[email protected]>---
credential/libsecret: load secrets explicitlyPublished-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(-)contrib/credential/libsecret/git-credential-libsecret.c +13
-1@@ -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
Hi Junio, thanks very much for your review. I also do not program Gnome, so this is just based on my best effort research :)
Quoted 9 lines · Junio C Hamano
The above makes it sound like we can just request with
secret_service_search_sync()exactly the same way as before (i.e., withLOAD_SECRETS), and then check withsecret_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_SECRETSreally 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.
Quoted 4 lines
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!
Friendly nudge :) Happy to look into any tweaks or questions.
Quoted 44 lines · Daniel Martí
Hi Junio, thanks very much for your review. I also do not program Gnome, so this is just based on my best effort research :)
Quoted 9 lines · Junio C Hamano
The above makes it sound like we can just request with
secret_service_search_sync()exactly the same way as before (i.e., withLOAD_SECRETS), and then check withsecret_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_SECRETSreally 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 implementsSECRET_SEARCH_LOAD_SECRETSas 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, soSECRET_SEARCH_UNLOCKhaving 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.
Quoted 4 lines
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!
Quoted 1 line · Daniel Martí
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.
Thanks for sharing
Quoted 29 lines · Daniel Martí via GitGitGadget
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
secret_service_search_sync()can return an item whose secret is not loaded, despiteSECRET_SEARCH_LOAD_SECRETSbeing 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 tosecret_value_get_text()andsecret_value_unref(), producing
secret_value_get_text: assertion 'value' failed
secret_value_unref: assertion 'value != NULL' failedand losing the password even when the secret is still retrievable.
Drop
SECRET_SEARCH_LOAD_SECRETSand instead load the secret of the one item we use withsecret_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.
Quoted 1 line
Signed-off-by Daniel Martí <[email protected]>
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
Quoted 49 lines
If
SECRET_SEARCH_LOAD_SECRETSis set in flags, then the items’ secret values will be loaded for any unlocked items. Loaded item secret values are available viasecret_item_get_secret(). If the load of a secret values fail, then the [mystery consequence]---
credential/libsecret: load secrets explicitlyPublished-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(-)contrib/credential/libsecret/git-credential-libsecret.c +13
-1@@ -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
Quoted 4 lines · M Hickford
Is this an upstream bug in libsecret?
The libsecret docs for
SECRET_SEARCH_LOAD_SECRETSare 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!
PATCH v2 1/1 credential/libsecret: load secrets explicitly
In reply to Daniel Martí via GitGitGadgetFrom: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
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.
---
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.
@@ 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(-)
contrib/credential/libsecret/git-credential-libsecret.c +13 -1
@@ -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
Signature · 1 line
gitgitgadget
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.
Quoted 149 lines · Daniel Martí via GitGitGadget
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
keyring_get()searches withSECRET_SEARCH_LOAD_SECRETS, then passessecret_item_get_secret()of the first match unchecked tosecret_value_get_text()andsecret_value_unref(). As libsecret documents, that secret can be NULL: the search does not load secrets of locked items, such as whenSECRET_SEARCH_UNLOCKfails 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' failedbefore 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_SECRETSis 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 withsecret_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í <[email protected]>---
credential/libsecret: load secrets explicitlyPublished-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/2372Range-diff vs v1:
1:
89d3eee91f! 1:b9ddf13fa1credential/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, despiteSECRET_SEARCH_LOAD_SECRETSbeing 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
- tosecret_value_get_text()andsecret_value_unref(), producing
+keyring_get()searches withSECRET_SEARCH_LOAD_SECRETS, then passes
+secret_item_get_secret()of the first match unchecked to
+secret_value_get_text()andsecret_value_unref(). As libsecret
+ documents, that secret can be NULL: the search does not load secrets
+ of locked items, such as whenSECRET_SEARCH_UNLOCKfails 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, butSECRET_SEARCH_LOAD_SECRETSis 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 withsecret_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_SECRETSand instead load the secret of the
- one item we use withsecret_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í <[email protected]>@@
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(-)contrib/credential/libsecret/git-credential-libsecret.c +13
-1@@ -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
Quoted 10 lines · Daniel Martí via GitGitGadget
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
keyring_get()searches withSECRET_SEARCH_LOAD_SECRETS, then passessecret_item_get_secret()of the first match unchecked to
...
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.
Quoted 2 lines
Signed-off-by Daniel Martí <[email protected]>---
It seems that unfortunately we saw interest or reviews from nobody since this v2 was sent. Anybody interested enough to review it now to resurrect?
Unless we see some volunteers to give it a review in a few weeks, I will discard the topic out of my tree, as 'seen' has been crowded with topics that have been collecting dust without getting many reviews.
Hi Daniel
Here are some comments from someone who uses the libsecret credential helper and has just spent some time reading the api docs (i.e. I don't have any experience writing code that uses libsecret).
Quoted 5 lines · Daniel Martí via GitGitGadget
From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <[email protected]>
keyring_get()searches withSECRET_SEARCH_LOAD_SECRETS, then passessecret_item_get_secret()of the first match unchecked tosecret_value_get_text()andsecret_value_unref().
This sentence is incredibly dense - I had to go and read the code to understand what it was saying.
Quoted 4 lines
As libsecret
documents, that secret can be NULL: the search does not load secrets
of locked items, such as whenSECRET_SEARCH_UNLOCKfails 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
Is that true? My reading of the documentation is that it includes the SecretItem in the list we get back from the search, but we cannot load the secret because it is locked.
Quoted 6 lines
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
Quoted 6 lines
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_SECRETSis not part of the search call:
libsecret implements it as a separate GetSecrets D-Bus call after SearchItems.
I don't see how this implementation detail is relevant. Surely what matters is the lack of error information, not how SECRET_SEARCH_LOAD_SECRETS is implemented.
Quoted 2 lines
Drop the flag and instead always load the one secret we use with
secret_item_load_secret_sync(), which reports errors.
If SECRET_SEARCH_UNLOCK fails and so we cannot load a secret because it is still locked, calling secret_item_load_secret_sync() does not tell us why the unlock failed. If we want to provide a better error message in that case I think we need to unlock the secret ourselves.
Quoted 1 line
This takes as many D-Bus calls as before,
Are we really that worried about the number of D-Bus calls? If we are then we should remove SECRET_SEARCH_UNLOCK and call secret_item_get_locked() to see whether we need to unlock it or not.
Quoted 4 lines
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.
secret-tool appears to use a completely different api to the code here
Quoted 1 line
An inaccessible item now produces a useful error message
The error message is printed with g_critical() which is intended for logging failed assertions[1], not printing runtime error messages. We should use
g_printerr("error: could not load secret: %s", error->message);instead. If we cannot load a secret because it has been erased by another process should we consider that an error as this patch does, or should we treat it like a missing credential behave as if the initial lookup had failed?
Quoted 4 lines
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.
I don't understand why needing a locked keyring means we cannot have a test. "secret-tool lock" will lock the keyring. There maybe other reasons that make it hard to test but it would be helpful to give a bit more detail.
contrib/credential/libsecret/git-credential-libsecret.c static int keyring_get(struct credential *c) · line 153
Quoted 2 lines
contrib/credential/libsecret/git-credential-libsecret.c +2
-0+if (!secret_item_load_secret_sync (item,NULL , &error)) { +g_critical ("could not load secret: %s" , error->message);
While this matches the existing code, we should be using g_printerr().
contrib/credential/libsecret/git-credential-libsecret.c static int keyring_get(struct credential *c) · line 155
Quoted 2 lines
contrib/credential/libsecret/git-credential-libsecret.c +2
-0+g_error_free (error); +g_list_free_full (items, g_object_unref);
It is a bit of a pain to have to free this in two paths. We could avoid that by doing
loaded = secret_item_load_secret_sync(item, NULL, &error);
g_list_free_full(items, g_object_unref);
if (!loaded) {
/* report error and return */
}and removing the call to g_list_free_full() at the end of the function. That would match how we handle freeing the hash table of attributes after the call to secret_service_search_sync().
contrib/credential/libsecret/git-credential-libsecret.c static int keyring_get(struct credential *c) · line 158
Quoted 3 lines
contrib/credential/libsecret/git-credential-libsecret.c +2
-0+return EXIT_FAILURE ; + } secret =secret_item_get_secret (item);
Are we sure this can never fail now, or should we check secret is non-NULL to avoid future problems?
Thanks
Phillip
Quoted 4 lines
attributes = secret_item_get_attributes(item);base-commit:
34f06850c16c7f7ac822b1adc71354f11b0f2ca3