Skip to content

wincred: fix line split of secret blob content - #2251

Open
becm wants to merge 1 commit into
gitgitgadget:masterfrom
becm:fix-wincred-secret-linesplit
Open

becm wants to merge 1 commit into
gitgitgadget:masterfrom
becm:fix-wincred-secret-linesplit

Conversation

@becm

@becm becm commented Oct 9, 2026 •

Copy link
Copy Markdown

Changes since v1:

  • move credential blob dissection and output to separate methods (maintainer request)
  • mark character constants as wide char
  • consistently treat sizes/lengths as (wide) character count

@gitgitgadget

gitgitgadget Bot commented Oct 9, 2026

Copy link
Copy Markdown

Welcome to GitGitGadget

Hi @becm, and welcome to GitGitGadget, the GitHub App to send patch series to the Git mailing list from GitHub Pull Requests.

Please make sure that either:

  • Your Pull Request has a good description, if it consists of multiple commits, as it will be used as cover letter.
  • Your Pull Request description is empty, if it consists of a single commit, as the commit message should be descriptive enough by itself.

You can CC potential reviewers by adding a footer to the PR description with the following syntax:

CC: Revi Ewer <revi.ewer@example.com>, Ill Takalook <ill.takalook@example.net>

NOTE: DO NOT copy/paste your CC list from a previous GGG PR's description,
because it will result in a malformed CC list on the mailing list. See
example.

Also, it is a good idea to review the commit messages one last time, as the Git project expects them in a quite specific form:

  • the lines should not exceed 76 columns,
  • the first line should be like a header and typically start with a prefix like "tests:" or "revisions:" to state which subsystem the change is about, and
  • the commit messages' body should be describing the "why?" of the change.
  • Finally, the commit messages should end in a Signed-off-by: line matching the commits' author.

It is in general a good idea to await the automated test ("Checks") in this Pull Request before contributing the patches, e.g. to avoid trivial issues such as unportable code.

Contributing the patches

Before you can contribute the patches, your GitHub username needs to be added to the list of permitted users. Any already-permitted user can do that, by adding a comment to your PR of the form /allow. A good way to find other contributors is to locate recent pull requests where someone has been /allowed:

Both the person who commented /allow and the PR author are able to /allow you.

An alternative is the channel #git-devel on the Libera Chat IRC network:

<newcontributor> I've just created my first PR, could someone please /allow me? https://github.com/gitgitgadget/git/pull/12345
<veteran> newcontributor: it is done
<newcontributor> thanks!

Once on the list of permitted usernames, you can contribute the patches to the Git mailing list by adding a PR comment /submit.

If you want to see what email(s) would be sent for a /submit request, add a PR comment /preview to have the email(s) sent to you. You must have a public GitHub email address for this. Note that any reviewers CC'd via the list in the PR description will not actually be sent emails.

After you submit, GitGitGadget will respond with another comment that contains the link to the cover letter mail in the Git mailing list archive. Please make sure to monitor the discussion in that thread and to address comments and suggestions (while the comments and suggestions will be mirrored into the PR by GitGitGadget, you will still want to reply via mail).

If you do not want to subscribe to the Git mailing list just to be able to respond to a mail, you can download the mbox from the Git mailing list archive (click the (raw) link), then import it into your mail program. If you use GMail, you can do this via:

curl -g --user "<EMailAddress>:<Password>" \
    --url "imaps://imap.gmail.com/INBOX" -T /path/to/raw.txt

To iterate on your change, i.e. send a revised patch or patch series, you will first want to (force-)push to the same branch. You probably also want to modify your Pull Request description (or title). It is a good idea to summarize the revision by adding something like this to the cover letter (read: by editing the first comment on the PR, i.e. the PR description):

Changes since v1:
- Fixed a typo in the commit message (found by ...)
- Added a code comment to ... as suggested by ...
...

To send a new iteration, just add another PR comment with the contents: /submit.

Need help?

New contributors who want advice are encouraged to join git-mentoring@googlegroups.com, where volunteers who regularly contribute to Git are willing to answer newbie questions, give advice, or otherwise provide mentoring to interested contributors. You must join in order to post or view messages, but anyone can join.

You may also be able to find help in real time in the developer IRC channel, #git-devel on Libera Chat. Remember that IRC does not support offline messaging, so if you send someone a private message and log out, they cannot respond to you. The scrollback of #git-devel is archived, though.

@gitgitgadget gitgitgadget Bot added the new user label Oct 9, 2026
@gitgitgadget

gitgitgadget Bot commented Oct 9, 2026

Copy link
Copy Markdown

There is an issue in commit bf066d4:
wincred: fix line split of secret blob content

  • Commit not signed off

@becm
becm force-pushed the fix-wincred-secret-linesplit branch from bf066d4 to fb80cfc Compare October 9, 2026 12:11
@dscho

dscho commented Oct 9, 2026

Copy link
Copy Markdown
Member

/allow

@gitgitgadget

gitgitgadget Bot commented Oct 9, 2026

Copy link
Copy Markdown

User becm is now allowed to use GitGitGadget.

WARNING: becm has no public email address set on GitHub; GitGitGadget needs an email address to Cc: you on your contribution, so that you receive any feedback on the Git mailing list. Go to https://github.com/settings/profile to make your preferred email public to let GitGitGadget know which email address to use.

@becm

becm commented Oct 9, 2026

Copy link
Copy Markdown
Author

/preview

@gitgitgadget

gitgitgadget Bot commented Oct 9, 2026

Copy link
Copy Markdown

Preview email sent as pull.2251.git.1791551247054.gitgitgadget@gmail.com

@becm

becm commented Oct 9, 2026

Copy link
Copy Markdown
Author

/submit

@gitgitgadget

gitgitgadget Bot commented Oct 9, 2026

Copy link
Copy Markdown

Submitted as pull.2251.git.1791553518774.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2251/becm/fix-wincred-secret-linesplit-v1

To fetch this version to local tag pr-2251/becm/fix-wincred-secret-linesplit-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2251/becm/fix-wincred-secret-linesplit-v1

@gitgitgadget

gitgitgadget Bot commented Oct 9, 2026

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Marc Becker via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Marc Becker <becm@gmx.de>
>
> operate on immutable blob data (wcsncpy_s still had invalid target size)
> split on newline character to avoid bleed-over on multi-line content

This needs a bit more work to make it more readable than a bulleted
list of lowercase fragments.

When in doubt, keep in mind that the usual way to compose a log
message of this project is to:

 - Give an observation on how the current system works in the
   present tense (so no need to say "Currently X is Y", or
   "Previously X was Y" to describe the state before your change;
   just "X is Y" is enough), and discuss what you perceive as a
   problem in it.

 - Propose a solution (optional---often, problem description
   trivially leads to an obvious solution in reader's minds).

 - Give commands to somebody editing the codebase to "make it so",
   instead of saying "This commit does X".

in this order.

 - It mentions wcsncpy_s having an invalid target size, but does not
   explain why it was invalid or the consequences. Is the issue that
   wcsncpy_s expects the buffer size in wide characters, but was
   being passed a size in bytes, which obviously cannot always
   agree?

 - It mentions "bleed-over on multi-line content", but does not
   describe the observable symptoms.  Is the issue that when the
   password is empty, the skipping by wcstok_s delimiter would cause
   the oauth_refresh_token line to be erroneously parsed as the
   password?

 - The final sentence should be an imperative command to the
   codebase, e.g., "Parse the blob in-place without copying and
   split lines manually using wmemchr()."

>
> Signed-off-by: Marc Becker <becm@gmx.de>
> ---
>     wincred: fix line split of secret blob content
>
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2251%2Fbecm%2Ffix-wincred-secret-linesplit-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2251/becm/fix-wincred-secret-linesplit-v1
> Pull-Request: https://github.com/gitgitgadget/git/pull/2251
>
>  .../wincred/git-credential-wincred.c          | 86 ++++++++++++-------
>  1 file changed, 55 insertions(+), 31 deletions(-)
>
> diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c
> index 22eb27ca31..584f457774 100644
> --- a/contrib/credential/wincred/git-credential-wincred.c
> +++ b/contrib/credential/wincred/git-credential-wincred.c
> @@ -6,6 +6,7 @@
>  #include <stdio.h>
>  #include <io.h>
>  #include <fcntl.h>
> +#include <wchar.h>
>  #include <wincred.h>
>  
>  /* common helpers */
> @@ -148,51 +149,74 @@ static void get_credential(void)
>  {
>  	CREDENTIALW **creds;
>  	DWORD num_creds;
> -	int i;
> -	CREDENTIAL_ATTRIBUTEW *attr;
> -	WCHAR *secret;
> -	WCHAR *line;
> -	WCHAR *remaining_lines;
> -	WCHAR *part;
> -	WCHAR *remaining_parts;
>  
>  	if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds))
>  		return;
>  
> -	/* search for the first credential that matches username */
> -	for (i = 0; i < num_creds; ++i)
> +	/* search for the first credential that matches target and username */
> +	for (int i = 0; i < num_creds; ++i) {
>  		if (match_cred(creds[i], 0)) {
> -			write_item("username", creds[i]->UserName,
> -				creds[i]->UserName ? wcslen(creds[i]->UserName) : 0);
> -			if (creds[i]->CredentialBlobSize > 0) {
> -				secret = xmalloc(creds[i]->CredentialBlobSize + sizeof(WCHAR));
> -				wcsncpy_s(secret, creds[i]->CredentialBlobSize, (LPCWSTR)creds[i]->CredentialBlob, creds[i]->CredentialBlobSize / sizeof(WCHAR));
> -				line = wcstok_s(secret, L"\r\n", &remaining_lines);
> -				write_item("password", line, line ? wcslen(line) : 0);
> -				while(line != NULL) {
> -					part = wcstok_s(line, L"=", &remaining_parts);
> -					if (!wcscmp(part, L"oauth_refresh_token")) {
> -						write_item("oauth_refresh_token", remaining_parts, remaining_parts ? wcslen(remaining_parts) : 0);
> -					}
> -					line = wcstok_s(NULL, L"\r\n", &remaining_lines);
> -				}
> -				free(secret);

The original was already bad, but this makes it even worse to have
the code nested too deeply.  Would separating out the body of the
for loop into a separate helper function, or perhaps standard tricks
like this

	for (...) {
		if (!match_cred(...))
			continue;
		... rest of the loop dedented by one tab stop ...
	}

make it readable?

> +			LPCWSTR username = creds[i]->UserName;
> +			LPCWSTR blob = (LPCWSTR)creds[i]->CredentialBlob;
> +			LPCWSTR end;
> +			DWORD wlen;
> +
> +			write_item("username", username, username ? wcslen(username) : 0);
> +
> +			wlen = creds[i]->CredentialBlobSize / sizeof(WCHAR);
> +
> +			// check if content is single line

			/* our single line comment should look like this */

> +			if ((end = wmemchr(blob, '\n', wlen)) == NULL) {

I do not do Windows and I do not often deal with wchar_t, so I do
not know how much practitioners of code like this one cares, but
would it be better to make the fact clear that we are not dealing
with a regular 'char' by writing a wchar_t literal like this as
L'\n'?  This is not a correctness suggestion, but a readability one.
Having a function prototype would coerse the parameter types, so
you may end up passing L'\n' either way.

> +				write_item("password", blob, wlen);
>  			} else {
> -				write_item("password",
> -						(LPCWSTR)creds[i]->CredentialBlob,
> -						creds[i]->CredentialBlobSize / sizeof(WCHAR));
> +				DWORD length = end++ - blob;

Here, "end" is of LPCWSTR type, aka "wchar_t *".  So is "blob".  The
difference would give us how many wide characters are in there.
That is not necessarily number of bytes starting at &blob[0].

> +				// correct remaining size and drop carriage return at line end
> +				wlen -= length + 1;
> +				if (length && blob[length - 1] == '\r') {

This CR is also side, right?

> +					--length;
> +				}
> +				write_item("password", blob, length);
> +
> +				// key/value content starting on next line
> +				blob = end;
> +				do {
> +					LPCWSTR value;
> +
> +					// find line end
> +					if ((end = wmemchr(blob, '\n', wlen)) == NULL) {
> +						length = wlen;
> +					} else {
> +						length = end++ - blob;
> +						// correct remaining size and drop carriage return at line end
> +						wlen -= length + 1;
> +						if (length && blob[length - 1] == '\r') {
> +							--length;
> +						}
> +					}
> +					// find key/value separator for extended credential info
> +					if ((value = wmemchr(blob, '=', length)) != NULL) {
> +						static const LPCWSTR refresh = L"oauth_refresh_token";
> +						DWORD klen = value - blob;

Value is also "wchar_t *", so klen counts the length in wchar_t,
which may be wider than a byte.  So is

> +						// write entries known to git credential protocol
> +						if (klen == wcslen(refresh) && memcmp(blob, refresh, klen) == 0) {

klen that counts number of wchar_t letters in refresh[] string.

So, is the memcmp() used to check if early part of blob[] match the
refresh[] as a whole correct, or is it only checking an early half
(or one fourth, depending on how much wider your wchar_t is compared
to char) of the string?

> +							write_item("oauth_refresh_token", value + 1, length - klen - 1);
> +						}
> +					}
> +				} while ((blob = end));
>  			}
>  			for (int j = 0; j < creds[i]->AttributeCount; j++) {
> -				attr = creds[i]->Attributes + j;
> +				CREDENTIAL_ATTRIBUTEW *attr = creds[i]->Attributes + j;
> +
>  				if (!wcscmp(attr->Keyword, L"git_password_expiry_utc")) {
> -					write_item("password_expiry_utc", (LPCWSTR)attr->Value,
> -					attr->ValueSize / sizeof(WCHAR));
> +					write_item("password_expiry_utc", (LPCWSTR)attr->Value, attr->ValueSize / sizeof(WCHAR));
>  					break;
>  				}
>  			}
>  			break;
>  		}
> -
> +	}
>  	CredFree(creds);
>  }
>  
>
> base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd

@becm
becm force-pushed the fix-wincred-secret-linesplit branch 2 times, most recently from 8867913 to da611dc Compare October 10, 2026 03:29
Invalid target size check (bytes instead of characters) for `wcsncpy_s`
already led to memory corruption; d22a488 just hid the error by making
sure the target is always big enough.
Single invocation of `wcstok_s` only cuts out first hit delimiter.
Consecutive items would always start with a line feed character if
separation consists of CRLF.
Only exception is 1st item (due to following bug).
Advancement to end of password line is missing. Password value is reused
as extended credential item but likely filtered out due to value
mismatch with accepted key.

Line split needs to be deterministic and code should be split up into
smaller blocks.

Create separate methods for writing credential items and the complete
credential blob content to tighten code in main credential loop.
Reduce variable scope and nesting level. Use early continue/return to
improve code readability.
Use `wmemchr` to reliably detect wide-character-newline in immutable
blob data without need to create a further copy.

Signed-off-by: Marc Becker <becm@gmx.de>
@becm
becm force-pushed the fix-wincred-secret-linesplit branch from da611dc to 01a1039 Compare October 10, 2026 03:59
@becm

becm commented Oct 10, 2026

Copy link
Copy Markdown
Author

/submit

@gitgitgadget

gitgitgadget Bot commented Oct 10, 2026

Copy link
Copy Markdown

Submitted as pull.2251.v2.git.1791632850780.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2251/becm/fix-wincred-secret-linesplit-v2

To fetch this version to local tag pr-2251/becm/fix-wincred-secret-linesplit-v2:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2251/becm/fix-wincred-secret-linesplit-v2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants