diff mbox series

[v2,5/6] connect: advertise OS version

Message ID 20250117104639.65608-6-usmanakinyemi202@gmail.com (mailing list archive)
State New
Headers show
Series Introduce os-version Capability with Configurable Options | expand

Commit Message

Usman Akinyemi Jan. 17, 2025, 10:46 a.m. UTC
As some issues that can happen with a Git client can be operating system
specific, it can be useful for a server to know which OS a client is
using. In the same way it can be useful for a client to know which OS
a server is using.

Let's introduce a new protocol (`os-version`) allowing Git clients and
servers to exchange operating system information. The protocol is
controlled by the new `transfer.advertiseOSVersion` config option.

Add the `transfer.advertiseOSVersion` config option to address
privacy concerns. It defaults to `true` and can be changed to
`false`. When enabled, this option makes clients and servers send each
other the OS name (e.g., "Linux" or "Windows"). The information is
retrieved using the 'sysname' field of the `uname(2)` system call.

However, there are differences between `uname(1)` (command-line utility)
and `uname(2)` (system call) outputs on Windows. These discrepancies
complicate testing on Windows platforms. For example:
  - `uname(1)` output: MINGW64_NT-10.0-20348.3.4.10-87d57229.x86_64\
  .2024-02-14.20:17.UTC.x86_64
  - `uname(2)` output: Windows.10.0.20348

On Windows, uname(2) is not actually system-supplied but is instead
already faked up by Git itself. We could have overcome the test issue
on Windows by implementing a new `uname` subcommand in `test-tool`
using uname(2), but except uname(2), which would be tested against
itself, there would be nothing platform specific, so it's just simpler
to disable the tests on Windows.

Mentored-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
---
 Documentation/config/transfer.txt |  7 ++++++
 Documentation/gitprotocol-v2.txt  | 18 +++++++++++++
 connect.c                         |  3 +++
 serve.c                           | 14 +++++++++++
 t/t5555-http-smart-common.sh      | 10 +++++++-
 t/t5701-git-serve.sh              | 20 +++++++++++++--
 t/test-lib-functions.sh           |  8 ++++++
 version.c                         | 42 +++++++++++++++++++++++++++++++
 version.h                         | 21 ++++++++++++++++
 9 files changed, 140 insertions(+), 3 deletions(-)

Comments

Junio C Hamano Jan. 17, 2025, 7:35 p.m. UTC | #1
Usman Akinyemi <usmanakinyemi202@gmail.com> writes:

> +os-version
> +~~~~~~~~~~
> +
> ...
> +characters(from 33 to 126 inclusive) and are typically made from the result of

Compared to the preceding few paragraphs, this paragraph is overly
wide (the previous iteration was much better).

I'll review this step separately later.
Junio C Hamano Jan. 17, 2025, 10:22 p.m. UTC | #2
Usman Akinyemi <usmanakinyemi202@gmail.com> writes:

> As some issues that can happen with a Git client can be operating system
> specific, it can be useful for a server to know which OS a client is
> using. In the same way it can be useful for a client to know which OS
> a server is using.

Hmph.  The other end may be running different version of Git, and
the version difference of _our_ software is probably more relevant.
For that matter, they may even be running something entirely
different from our software, like Gerrit.  So I am not sure I am
convinced that os-version thing is a good thing to have with that
paragraph.

> Let's introduce a new protocol (`os-version`) allowing Git clients and
> servers to exchange operating system information. The protocol is
> controlled by the new `transfer.advertiseOSVersion` config option.

The last sentence is redundant and can safely removed.  The next
paragraph describes it better than "is controlled by".

> Add the `transfer.advertiseOSVersion` config option to address
> privacy concerns. It defaults to `true` and can be changed to
> `false`. When enabled, this option makes clients and servers send each
> other the OS name (e.g., "Linux" or "Windows"). The information is
> retrieved using the 'sysname' field of the `uname(2)` system call.

Add "or its equivalent" at the end.

macOS may have one, but it probably is not quite correct to say that
Windows have uname system call (otherwise we wouldn't be emulating
it on top of GetVersion ourselves).

> However, there are differences between `uname(1)` (command-line utility)
> and `uname(2)` (system call) outputs on Windows. These discrepancies
> complicate testing on Windows platforms. For example:
>   - `uname(1)` output: MINGW64_NT-10.0-20348.3.4.10-87d57229.x86_64\
>   .2024-02-14.20:17.UTC.x86_64
>   - `uname(2)` output: Windows.10.0.20348
>
> On Windows, uname(2) is not actually system-supplied but is instead
> already faked up by Git itself. We could have overcome the test issue
> on Windows by implementing a new `uname` subcommand in `test-tool`
> using uname(2), but except uname(2), which would be tested against
> itself, there would be nothing platform specific, so it's just simpler
> to disable the tests on Windows.

OK.

> +transfer.advertiseOSVersion::
> +	When `true`, the `os-version` capability is advertised by clients and
> +	servers. It makes clients and servers send to each other a string
> +	representing the operating system name, like "Linux" or "Windows".
> +	This string is retrieved from the `sysname` field of the struct returned
> +	by the uname(2) system call. Defaults to true.

Presumably, both ends of the connection independently choose whether
they enable or disable this variable, so we have 2x2=4 combinations
(here, versions of Git before the os-version capability support is
introduced behave the same way as an installation with this
configuration variable set to false).

And among these four combinations, only one of them results in "send
to each other", but the description above is fuzzy.

> diff --git a/connect.c b/connect.c
> index 10fad43e98..6d5792b63c 100644
> --- a/connect.c
> +++ b/connect.c
> @@ -492,6 +492,9 @@ static void send_capabilities(int fd_out, struct packet_reader *reader)
>  	if (server_supports_v2("agent"))
>  		packet_write_fmt(fd_out, "agent=%s", git_user_agent_sanitized());
>  
> +	if (server_supports_v2("os-version") && advertise_os_version(the_repository))
> +		packet_write_fmt(fd_out, "os-version=%s", os_version_sanitized());

Not a new problem, because the new code is pretty-much a straight
copy from the existing "agent" code, but do we ever use unsanitized
versions of git-user-agent and os-version?  If not, I am wondering
if we should sanitize immediately when we obtain the raw string and
keep it, get rid of _santized() function from the public API, and
make anybody calling git_user_agent() and os_version() to get
sanitized safe-to-use strings.

I see http.c throws git_user_agent() without doing any sanitization
at the cURL library, but it may be a mistake that we may want to fix
(outside the scope of this topic).  Since the contrast between the
os_version() vs the os_version_sanitized() is *new* in this series,
however, we probably would want to get it right from the beginning.

So the question is again, do we ever need to use os_version() that
is a raw string that may require sanitizing?  I do not think of any
offhand.
Randall S. Becker Jan. 17, 2025, 10:47 p.m. UTC | #3
On January 17, 2025 5:22 PM, Junio C Hamano wrote:
>Usman Akinyemi <usmanakinyemi202@gmail.com> writes:
>
>> As some issues that can happen with a Git client can be operating
>> system specific, it can be useful for a server to know which OS a
>> client is using. In the same way it can be useful for a client to know
>> which OS a server is using.
>
>Hmph.  The other end may be running different version of Git, and the
version
>difference of _our_ software is probably more relevant.
>For that matter, they may even be running something entirely different from
our
>software, like Gerrit.  So I am not sure I am convinced that os-version
thing is a good
>thing to have with that paragraph.
>
>> Let's introduce a new protocol (`os-version`) allowing Git clients and
>> servers to exchange operating system information. The protocol is
>> controlled by the new `transfer.advertiseOSVersion` config option.
>
>The last sentence is redundant and can safely removed.  The next paragraph
>describes it better than "is controlled by".
>
>> Add the `transfer.advertiseOSVersion` config option to address privacy
>> concerns. It defaults to `true` and can be changed to `false`. When
>> enabled, this option makes clients and servers send each other the OS
>> name (e.g., "Linux" or "Windows"). The information is retrieved using
>> the 'sysname' field of the `uname(2)` system call.
>
>Add "or its equivalent" at the end.

>macOS may have one, but it probably is not quite correct to say that
Windows have
>uname system call (otherwise we wouldn't be emulating it on top of
GetVersion
>ourselves).
>
>> However, there are differences between `uname(1)` (command-line
>> utility) and `uname(2)` (system call) outputs on Windows. These
>> discrepancies complicate testing on Windows platforms. For example:
>>   - `uname(1)` output: MINGW64_NT-10.0-20348.3.4.10-87d57229.x86_64\
>>   .2024-02-14.20:17.UTC.x86_64
>>   - `uname(2)` output: Windows.10.0.20348
>>
>> On Windows, uname(2) is not actually system-supplied but is instead
>> already faked up by Git itself. We could have overcome the test issue
>> on Windows by implementing a new `uname` subcommand in `test-tool`
>> using uname(2), but except uname(2), which would be tested against
>> itself, there would be nothing platform specific, so it's just simpler
>> to disable the tests on Windows.
>
>OK.
>
>> +transfer.advertiseOSVersion::
>> +	When `true`, the `os-version` capability is advertised by clients
and
>> +	servers. It makes clients and servers send to each other a string
>> +	representing the operating system name, like "Linux" or "Windows".
>> +	This string is retrieved from the `sysname` field of the struct
returned
>> +	by the uname(2) system call. Defaults to true.
>
>Presumably, both ends of the connection independently choose whether they
>enable or disable this variable, so we have 2x2=4 combinations (here,
versions of
>Git before the os-version capability support is introduced behave the same
way as
>an installation with this configuration variable set to false).
>
>And among these four combinations, only one of them results in "send to
each
>other", but the description above is fuzzy.
>
>> diff --git a/connect.c b/connect.c
>> index 10fad43e98..6d5792b63c 100644
>> --- a/connect.c
>> +++ b/connect.c
>> @@ -492,6 +492,9 @@ static void send_capabilities(int fd_out, struct
>packet_reader *reader)
>>  	if (server_supports_v2("agent"))
>>  		packet_write_fmt(fd_out, "agent=%s",
git_user_agent_sanitized());
>>
>> +	if (server_supports_v2("os-version") &&
>advertise_os_version(the_repository))
>> +		packet_write_fmt(fd_out, "os-version=%s",
>os_version_sanitized());
>
>Not a new problem, because the new code is pretty-much a straight copy from
the
>existing "agent" code, but do we ever use unsanitized versions of
git-user-agent and
>os-version?  If not, I am wondering if we should sanitize immediately when
we
>obtain the raw string and keep it, get rid of _santized() function from the
public API,
>and make anybody calling git_user_agent() and os_version() to get sanitized
safe-
>to-use strings.
>
>I see http.c throws git_user_agent() without doing any sanitization at the
cURL
>library, but it may be a mistake that we may want to fix (outside the scope
of this
>topic).  Since the contrast between the
>os_version() vs the os_version_sanitized() is *new* in this series,
however, we
>probably would want to get it right from the beginning.
>
>So the question is again, do we ever need to use os_version() that is a raw
string
>that may require sanitizing?  I do not think of any offhand.

uname(2) is definitely not portable. uname(1) is almost always available,
but
there is no guarantee about uname(2). I am not entirely happy having my
builds break if having to write one between rc0 and rc1 when this rolls. How
is this being handled? os_version() is also not portable. What if we had
something that asked for specific elements of the string, by name or id.
Junio C Hamano Jan. 17, 2025, 11:04 p.m. UTC | #4
<rsbecker@nexbridge.com> writes:

>>So the question is again, do we ever need to use os_version() that is a raw
> string
>>that may require sanitizing?  I do not think of any offhand.
>
> uname(2) is definitely not portable. uname(1) is almost always available,
> but
> there is no guarantee about uname(2). I am not entirely happy having my
> builds break if having to write one between rc0 and rc1 when this rolls. How
> is this being handled? os_version() is also not portable. What if we had
> something that asked for specific elements of the string, by name or id.

Sorry, I fail to see anything in your paragraph that is relevant to
what I said.  Especially os_version() is a function that is
implemented in the patchset, not something you would complain about
being "not portable".
Usman Akinyemi Jan. 20, 2025, 6:15 p.m. UTC | #5
On Sat, Jan 18, 2025 at 3:52 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Usman Akinyemi <usmanakinyemi202@gmail.com> writes:
>
> > As some issues that can happen with a Git client can be operating system
> > specific, it can be useful for a server to know which OS a client is
> > using. In the same way it can be useful for a client to know which OS
> > a server is using.
>
> Hmph.  The other end may be running different version of Git, and
> the version difference of _our_ software is probably more relevant.
> For that matter, they may even be running something entirely
> different from our software, like Gerrit.  So I am not sure I am
> convinced that os-version thing is a good thing to have with that
> paragraph.
Hi Junio,

What could be a better way of describing this ? Also, user-agent capability is
already sharing the information about the version of Git.
>
> > Let's introduce a new protocol (`os-version`) allowing Git clients and
> > servers to exchange operating system information. The protocol is
> > controlled by the new `transfer.advertiseOSVersion` config option.
>
> The last sentence is redundant and can safely removed.  The next
> paragraph describes it better than "is controlled by".
Noted, I will do that.
>
> > Add the `transfer.advertiseOSVersion` config option to address
> > privacy concerns. It defaults to `true` and can be changed to
> > `false`. When enabled, this option makes clients and servers send each
> > other the OS name (e.g., "Linux" or "Windows"). The information is
> > retrieved using the 'sysname' field of the `uname(2)` system call.
>
> Add "or its equivalent" at the end.
>
> macOS may have one, but it probably is not quite correct to say that
> Windows have uname system call (otherwise we wouldn't be emulating
> it on top of GetVersion ourselves).
Yeah, noted. I will do that in the next iteration.
>
> > However, there are differences between `uname(1)` (command-line utility)
> > and `uname(2)` (system call) outputs on Windows. These discrepancies
> > complicate testing on Windows platforms. For example:
> >   - `uname(1)` output: MINGW64_NT-10.0-20348.3.4.10-87d57229.x86_64\
> >   .2024-02-14.20:17.UTC.x86_64
> >   - `uname(2)` output: Windows.10.0.20348
> >
> > On Windows, uname(2) is not actually system-supplied but is instead
> > already faked up by Git itself. We could have overcome the test issue
> > on Windows by implementing a new `uname` subcommand in `test-tool`
> > using uname(2), but except uname(2), which would be tested against
> > itself, there would be nothing platform specific, so it's just simpler
> > to disable the tests on Windows.
>
> OK.
>
> > +transfer.advertiseOSVersion::
> > +     When `true`, the `os-version` capability is advertised by clients and
> > +     servers. It makes clients and servers send to each other a string
> > +     representing the operating system name, like "Linux" or "Windows".
> > +     This string is retrieved from the `sysname` field of the struct returned
> > +     by the uname(2) system call. Defaults to true.
>
> Presumably, both ends of the connection independently choose whether
> they enable or disable this variable, so we have 2x2=4 combinations
> (here, versions of Git before the os-version capability support is
> introduced behave the same way as an installation with this
> configuration variable set to false).
>
> And among these four combinations, only one of them results in "send
> to each other", but the description above is fuzzy.
Yeah, describing the four combinations would better right ?
>
> > diff --git a/connect.c b/connect.c
> > index 10fad43e98..6d5792b63c 100644
> > --- a/connect.c
> > +++ b/connect.c
> > @@ -492,6 +492,9 @@ static void send_capabilities(int fd_out, struct packet_reader *reader)
> >       if (server_supports_v2("agent"))
> >               packet_write_fmt(fd_out, "agent=%s", git_user_agent_sanitized());
> >
> > +     if (server_supports_v2("os-version") && advertise_os_version(the_repository))
> > +             packet_write_fmt(fd_out, "os-version=%s", os_version_sanitized());
>
> Not a new problem, because the new code is pretty-much a straight
> copy from the existing "agent" code, but do we ever use unsanitized
> versions of git-user-agent and os-version?  If not, I am wondering
> if we should sanitize immediately when we obtain the raw string and
> keep it, get rid of _santized() function from the public API, and
> make anybody calling git_user_agent() and os_version() to get
> sanitized safe-to-use strings.
>
> I see http.c throws git_user_agent() without doing any sanitization
> at the cURL library, but it may be a mistake that we may want to fix
> (outside the scope of this topic).  Since the contrast between the
> os_version() vs the os_version_sanitized() is *new* in this series,
> however, we probably would want to get it right from the beginning.
>
> So the question is again, do we ever need to use os_version() that
> is a raw string that may require sanitizing?  I do not think of any
> offhand.
In this case, I guess there has to be a conclusion on what to do.
Junio C Hamano Jan. 21, 2025, 7:06 p.m. UTC | #6
Usman Akinyemi <usmanakinyemi202@gmail.com> writes:

> On Sat, Jan 18, 2025 at 3:52 AM Junio C Hamano <gitster@pobox.com> wrote:
>>
>> Usman Akinyemi <usmanakinyemi202@gmail.com> writes:
>>
>> > As some issues that can happen with a Git client can be operating system
>> > specific, it can be useful for a server to know which OS a client is
>> > using. In the same way it can be useful for a client to know which OS
>> > a server is using.
>>
>> Hmph.  The other end may be running different version of Git, and
>> the version difference of _our_ software is probably more relevant.
>> For that matter, they may even be running something entirely
>> different from our software, like Gerrit.  So I am not sure I am
>> convinced that os-version thing is a good thing to have with that
>> paragraph.
> Hi Junio,
>
> What could be a better way of describing this ? Also, user-agent capability is
> already sharing the information about the version of Git.

I dunno.  I only said that what you said does not convince me that
os-version is a good thing.  Try harder, perhaps, to be more
convincing?  After all it is your itch.

An alternative that may be conceptually cleaner is to encourage
people to include not just Git version but OS variant information in
the existing "agent" capability, making it easier to do (which
probably means an addition to configuration knob), and encourage
implementors of other Git-compatible software to also let their
systems identify themselves via the "agent" capability.

As we have documented that "agent" strings are purely informative,
there shouldn't be any problem if we started identifying the version
of Git running on one end as "git/2.47.0 Linux" (instead of
"git/2.47.0").

>> And among these four combinations, only one of them results in "send
>> to each other", but the description above is fuzzy.
> Yeah, describing the four combinations would better right ?

I do not think readers necessarily would want to hear about the four
combinations; a paragraph that makes it clear that the configuration
is independently set on either end of the connection will make it
obvious to them without being told.

>> So the question is again, do we ever need to use os_version() that
>> is a raw string that may require sanitizing?  I do not think of any
>> offhand.
> In this case, I guess there has to be a conclusion on what to do.

As I didn't hear any concrete use cases of the version string before
sanitizing, I would suggest to make os_version() to

 - ask for a string from the underlying system layer (like uname(2)
   or its emulation), 

 - immediately sanitize it,

 - cache the result from the above (just like the
   os_version_sanitized() in the posted patch does with a variable
   of type "static char *" in the function scope), and

 - keep returning that sanitized version to the callers.

to simplify the API surface.

Thanks.
diff mbox series

Patch

diff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt
index f1ce50f4a6..c368a893bd 100644
--- a/Documentation/config/transfer.txt
+++ b/Documentation/config/transfer.txt
@@ -125,3 +125,10 @@  transfer.bundleURI::
 transfer.advertiseObjectInfo::
 	When `true`, the `object-info` capability is advertised by
 	servers. Defaults to false.
+
+transfer.advertiseOSVersion::
+	When `true`, the `os-version` capability is advertised by clients and
+	servers. It makes clients and servers send to each other a string
+	representing the operating system name, like "Linux" or "Windows".
+	This string is retrieved from the `sysname` field of the struct returned
+	by the uname(2) system call. Defaults to true.
diff --git a/Documentation/gitprotocol-v2.txt b/Documentation/gitprotocol-v2.txt
index 1652fef3ae..a332b55e4c 100644
--- a/Documentation/gitprotocol-v2.txt
+++ b/Documentation/gitprotocol-v2.txt
@@ -190,6 +190,24 @@  printable ASCII characters except space (i.e., the byte range 32 < x <
 and debugging purposes, and MUST NOT be used to programmatically assume
 the presence or absence of particular features.
 
+os-version
+~~~~~~~~~~
+
+In the same way as the `agent` capability above, the server can
+advertise the `os-version` capability to notify the client the
+kind of operating system it is running on. The client may optionally
+send its own `os-version` capability, to notify the server the kind of
+operating system it is also running on in its request to the server
+(but it MUST NOT do so if the server did not advertise the os-version
+capability). The value of this capability may consist of ASCII printable
+characters(from 33 to 126 inclusive) and are typically made from the result of
+`uname -s`(OS name e.g Linux). The os-version capability can be disabled
+entirely by setting the `transfer.advertiseOSVersion` config option
+to `false`. The `os-version` strings are purely informative for
+statistics and debugging purposes, and MUST NOT be used to
+programmatically assume the presence or absence of particular
+features.
+
 ls-refs
 ~~~~~~~
 
diff --git a/connect.c b/connect.c
index 10fad43e98..6d5792b63c 100644
--- a/connect.c
+++ b/connect.c
@@ -492,6 +492,9 @@  static void send_capabilities(int fd_out, struct packet_reader *reader)
 	if (server_supports_v2("agent"))
 		packet_write_fmt(fd_out, "agent=%s", git_user_agent_sanitized());
 
+	if (server_supports_v2("os-version") && advertise_os_version(the_repository))
+		packet_write_fmt(fd_out, "os-version=%s", os_version_sanitized());
+
 	if (server_feature_v2("object-format", &hash_name)) {
 		int hash_algo = hash_algo_by_name(hash_name);
 		if (hash_algo == GIT_HASH_UNKNOWN)
diff --git a/serve.c b/serve.c
index c8694e3751..5b0d54ae9a 100644
--- a/serve.c
+++ b/serve.c
@@ -31,6 +31,16 @@  static int agent_advertise(struct repository *r UNUSED,
 	return 1;
 }
 
+static int os_version_advertise(struct repository *r,
+			   struct strbuf *value)
+{
+	if (!advertise_os_version(r))
+		return 0;
+	if (value)
+		strbuf_addstr(value, os_version_sanitized());
+	return 1;
+}
+
 static int object_format_advertise(struct repository *r,
 				   struct strbuf *value)
 {
@@ -123,6 +133,10 @@  static struct protocol_capability capabilities[] = {
 		.name = "agent",
 		.advertise = agent_advertise,
 	},
+	{
+		.name = "os-version",
+		.advertise = os_version_advertise,
+	},
 	{
 		.name = "ls-refs",
 		.advertise = ls_refs_advertise,
diff --git a/t/t5555-http-smart-common.sh b/t/t5555-http-smart-common.sh
index e47ea1ad10..6f357a005a 100755
--- a/t/t5555-http-smart-common.sh
+++ b/t/t5555-http-smart-common.sh
@@ -123,9 +123,17 @@  test_expect_success 'git receive-pack --advertise-refs: v1' '
 '
 
 test_expect_success 'git upload-pack --advertise-refs: v2' '
+	printf "agent=FAKE" >agent_and_osversion &&
+	if test_have_prereq WINDOWS
+	then
+		git config transfer.advertiseOSVersion false
+	else
+		printf "\nos-version=%s\n" $(uname -s | test_redact_non_printables) >>agent_and_osversion
+	fi &&
+
 	cat >expect <<-EOF &&
 	version 2
-	agent=FAKE
+	$(cat agent_and_osversion)
 	ls-refs=unborn
 	fetch=shallow wait-for-done
 	server-option
diff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh
index 0c0a5b2aec..8a783b3924 100755
--- a/t/t5701-git-serve.sh
+++ b/t/t5701-git-serve.sh
@@ -26,10 +26,26 @@  test_expect_success 'setup to generate files with expected content' '
 	cat >expect.trailer <<-EOF &&
 	0000
 	EOF
+
+	if test_have_prereq WINDOWS
+	then
+		git config transfer.advertiseOSVersion false
+	else
+		printf "\nos-version=%s\n" $(uname -s | test_redact_non_printables) >>agent_and_osversion
+	fi &&
+
+	cat >expect_osversion.base <<-EOF
+	version 2
+	$(cat agent_and_osversion)
+	ls-refs=unborn
+	fetch=shallow wait-for-done
+	server-option
+	object-format=$(test_oid algo)
+	EOF
 '
 
 test_expect_success 'test capability advertisement' '
-	cat expect.base expect.trailer >expect &&
+	cat expect_osversion.base expect.trailer >expect &&
 
 	GIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \
 		--advertise-capabilities >out &&
@@ -357,7 +373,7 @@  test_expect_success 'test capability advertisement with uploadpack.advertiseBund
 	cat >expect.extra <<-EOF &&
 	bundle-uri
 	EOF
-	cat expect.base \
+	cat expect_osversion.base \
 	    expect.extra \
 	    expect.trailer >expect &&
 
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 78e054ab50..f7ff38521c 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -2007,3 +2007,11 @@  test_trailing_hash () {
 		test-tool hexdump |
 		sed "s/ //g"
 }
+
+# Trim and replace each character with ascii code below 32 or above
+# 127 (included) using a dot '.' character.
+# Octal intervals \001-\040 and \177-\377
+# corresponds to decimal intervals 1-32 and 127-255
+test_redact_non_printables () {
+    tr -d "\n\r" | tr "[\001-\040][\177-\377]" "."
+}
diff --git a/version.c b/version.c
index 46835ec83f..ea334c3e9c 100644
--- a/version.c
+++ b/version.c
@@ -3,6 +3,7 @@ 
 #include "version-def.h"
 #include "strbuf.h"
 #include "gettext.h"
+#include "config.h"
 
 const char git_version_string[] = GIT_VERSION;
 const char git_built_from_commit_string[] = GIT_BUILT_FROM_COMMIT;
@@ -69,3 +70,44 @@  int get_uname_info(struct strbuf *buf, unsigned int full)
 	     strbuf_addf(buf, "%s\n", uname_info.sysname);
 	return 0;
 }
+
+const char *os_version(void)
+{
+	static const char *os = NULL;
+
+	if (!os) {
+		struct strbuf buf = STRBUF_INIT;
+
+		get_uname_info(&buf, 0);
+		os = strbuf_detach(&buf, NULL);
+	}
+
+	return os;
+}
+
+const char *os_version_sanitized(void)
+{
+	static const char *os_sanitized = NULL;
+
+	if (!os_sanitized) {
+		struct strbuf buf = STRBUF_INIT;
+
+		strbuf_addstr(&buf, os_version());
+		redact_non_printables(&buf);
+		os_sanitized = strbuf_detach(&buf, NULL);
+	}
+
+	return os_sanitized;
+}
+
+int advertise_os_version(struct repository *r)
+{
+	static int transfer_advertise_os_version = -1;
+
+	if (transfer_advertise_os_version == -1) {
+		repo_config_get_bool(r, "transfer.advertiseosversion", &transfer_advertise_os_version);
+		/* enabled by default */
+		transfer_advertise_os_version = !!transfer_advertise_os_version;
+	}
+	return transfer_advertise_os_version;
+}
diff --git a/version.h b/version.h
index 5eb586c0bd..3e983bc623 100644
--- a/version.h
+++ b/version.h
@@ -1,6 +1,8 @@ 
 #ifndef VERSION_H
 #define VERSION_H
 
+struct repository;
+
 extern const char git_version_string[];
 extern const char git_built_from_commit_string[];
 
@@ -14,4 +16,23 @@  const char *git_user_agent_sanitized(void);
 */
 int get_uname_info(struct strbuf *buf, unsigned int full);
 
+/*
+  Retrieve and cache system information for subsequent calls.
+  Return a pointer to the cached system information string.
+*/
+const char *os_version(void);
+
+/*
+  Retrieve system information string from os_version(). Then
+  sanitize and cache it. Return a pointer to the sanitized
+  system information string.
+*/
+const char *os_version_sanitized(void);
+
+/*
+  Retrieve and cache whether os-version capability is enabled.
+  Return 1 if enabled, 0 if disabled.
+*/
+int advertise_os_version(struct repository *r);
+
 #endif /* VERSION_H */