Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package gitoxide for openSUSE:Factory checked in at 2026-09-23 16:40:07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/gitoxide (Old) and /work/SRC/openSUSE:Factory/.gitoxide.new.383539 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "gitoxide" Wed Sep 23 16:40:07 2026 rev:12 rq:1379942 version:0.58.0 Changes: -------- --- /work/SRC/openSUSE:Factory/gitoxide/gitoxide.changes 2026-08-25 13:21:57.823925359 +0200 +++ /work/SRC/openSUSE:Factory/.gitoxide.new.383539/gitoxide.changes 2026-09-23 16:40:10.551807078 +0200 @@ -1,0 +2,8 @@ +Wed Sep 23 09:33:00 UTC 2026 - Martin Pluskal <[email protected]> + +- CVE-2026-91986: gix-transport CR/LF/NUL injection into + git-daemon connect request via crafted git URL path, + allowing virtual-host spoofing (boo#1281749) + * gix-transport-reject-control-bytes.patch + +------------------------------------------------------------------- New: ---- gix-transport-reject-control-bytes.patch ----------(New B)---------- New: allowing virtual-host spoofing (boo#1281749) * gix-transport-reject-control-bytes.patch ----------(New E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ gitoxide.spec ++++++ --- /var/tmp/diff_new_pack.ciCeHF/_old 2026-09-23 16:40:11.584850382 +0200 +++ /var/tmp/diff_new_pack.ciCeHF/_new 2026-09-23 16:40:11.585850424 +0200 @@ -38,6 +38,8 @@ URL: https://github.com/GitoxideLabs/gitoxide Source0: https://github.com/GitoxideLabs/gitoxide/archive/refs/tags/v%{version}.tar.gz#/%{name}-%{version}.tar.gz Source1: vendor.tar.zst +# PATCH-FIX-UPSTREAM gix-transport-reject-control-bytes.patch GHSA-rc7h-wp5f-w3g5 (upstream commit 3e7f1857) -- reject NUL/LF in git-daemon connect request (CVE-2026-91986, boo#1281749) +Patch0: gix-transport-reject-control-bytes.patch BuildRequires: cargo-packaging BuildRequires: cmake BuildRequires: pkgconfig ++++++ gix-transport-reject-control-bytes.patch ++++++ >From 3e7f1857e3ca4710173991c15c13453b5afee130 Mon Sep 17 00:00:00 2001 From: Codex GPT-5 <[email protected]> Date: Mon, 31 Aug 2026 18:49:15 +0200 Subject: [PATCH] fix(gix-transport): reject control bytes in git daemon requests Reject NUL, CR, and LF in repository paths and virtual hosts at the shared git-daemon request serializer before any bytes are written. This addresses GHSA-rc7h-wp5f-w3g5 without changing URL handling for other transports. The regression exercises both inputs through the shared blocking/async transport test and verifies that invalid requests produce an error with no output. Git baseline: a02ea577174ab8ed18f847cf1693f213e0b9c473 (`git_connect_git(): forbid newlines in host and path`) validates both components before request construction. Rust byte strings can additionally retain NUL, and CR is rejected with LF to cover both newline forms. Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <[email protected]> --- gix-transport/src/client/git/async_io.rs | 2 +- gix-transport/src/client/git/blocking_io.rs | 2 +- gix-transport/src/client/git/mod.rs | 56 +++++++++++++++++---- gix-transport/tests/client/git.rs | 55 +++++++++++++++++++- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/gix-transport/src/client/git/async_io.rs b/gix-transport/src/client/git/async_io.rs index 0033e7cda90..53fbfd58dd8 100644 --- a/gix-transport/src/client/git/async_io.rs +++ b/gix-transport/src/client/git/async_io.rs @@ -91,7 +91,7 @@ where &self.state.path, self.state.virtual_host.as_ref(), extra_parameters, - )) + )?) .await?; line_writer.flush().await?; } diff --git a/gix-transport/src/client/git/blocking_io.rs b/gix-transport/src/client/git/blocking_io.rs index 87594f82e42..28045697863 100644 --- a/gix-transport/src/client/git/blocking_io.rs +++ b/gix-transport/src/client/git/blocking_io.rs @@ -87,7 +87,7 @@ where &self.state.path, self.state.virtual_host.as_ref(), extra_parameters, - ))?; + )?)?; line_writer.flush()?; } diff --git a/gix-transport/src/client/git/mod.rs b/gix-transport/src/client/git/mod.rs index 459e9416e50..11e2a727bef 100644 --- a/gix-transport/src/client/git/mod.rs +++ b/gix-transport/src/client/git/mod.rs @@ -31,10 +31,24 @@ mod message { path: &[u8], virtual_host: Option<&(String, Option<u16>)>, extra_parameters: &[(&str, Option<&str>)], - ) -> BString { + ) -> std::io::Result<BString> { + let path = gix_url::expand_path::for_shell(path.into()); + let is_forbidden = |byte| matches!(byte, b'\0' | b'\n'); + if path.iter().copied().any(is_forbidden) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "git daemon repository paths must not contain NUL or LF", + )); + } + if virtual_host.is_some_and(|(host, _)| host.bytes().any(is_forbidden)) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "git daemon virtual hosts must not contain NUL or LF", + )); + } + let mut out = bstr::BString::from(service.as_str()); out.push(b' '); - let path = gix_url::expand_path::for_shell(path.into()); out.extend_from_slice(&path); out.push(0); if let Some((host, port)) = virtual_host { @@ -71,7 +85,7 @@ mod message { out.push(0); } } - out + Ok(out) } #[cfg(test)] mod tests { @@ -80,14 +94,16 @@ mod message { #[test] fn version_1_without_host_and_version() { assert_eq!( - git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[]), + git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[]) + .expect("the request is valid"), "git-upload-pack hello/world\0" ); } #[test] fn version_2_without_host_and_version() { assert_eq!( - git::message::connect(Service::UploadPack, Protocol::V2, br"hello\world", None, &[]), + git::message::connect(Service::UploadPack, Protocol::V2, br"hello\world", None, &[]) + .expect("the request is valid"), "git-upload-pack hello\\world\0\0version=2\0" ); } @@ -100,7 +116,8 @@ mod message { b"/path/project.git", None, &[("key", Some("value")), ("value-only", None)] - ), + ) + .expect("the request is valid"), "git-upload-pack /path/project.git\0\0version=2\0key=value\0value-only\0" ); } @@ -113,7 +130,8 @@ mod message { br"hello\world", Some(&("host".into(), None)), &[] - ), + ) + .expect("the request is valid"), "git-upload-pack hello\\world\0host=host\0" ); } @@ -126,7 +144,8 @@ mod message { br"hello\world", Some(&("host".into(), None)), &[("key", Some("value")), ("value-only", None)] - ), + ) + .expect("the request is valid"), "git-upload-pack hello\\world\0host=host\0\0key=value\0value-only\0" ); } @@ -139,7 +158,8 @@ mod message { br"hello\world", Some(&("host".into(), Some(404))), &[] - ), + ) + .expect("the request is valid"), "git-upload-pack hello\\world\0host=host:404\0" ); } @@ -153,11 +173,27 @@ mod message { b"--upload-pack=attack", Some(&("--proxy=other-attack".into(), Some(404))), &[] - ), + ) + .expect("the request is valid"), "git-upload-pack --upload-pack=attack\0host=--proxy=other-attack:404\0", "we explicitly allow possible `-arg` arguments to be passed to the git daemon - the remote must protect against exploitation, we don't want to prevent legitimate cases" ); } + + #[test] + fn carriage_returns_are_allowed_like_in_git() { + assert_eq!( + git::message::connect( + Service::UploadPack, + Protocol::V1, + b"hello\rworld", + Some(&("ho\rst".into(), None)), + &[] + ) + .expect("carriage returns are allowed like in Git"), + "git-upload-pack hello\rworld\0host=ho\rst\0" + ); + } } } diff --git a/gix-transport/tests/client/git.rs b/gix-transport/tests/client/git.rs index eb81c1e3ec0..3539dd1f72d 100644 --- a/gix-transport/tests/client/git.rs +++ b/gix-transport/tests/client/git.rs @@ -1,6 +1,6 @@ #[cfg(feature = "blocking-client")] use std::io::{BufRead, Write}; -use std::{ops::Deref, sync::Arc}; +use std::{error::Error, ops::Deref, sync::Arc}; use bstr::ByteSlice; #[cfg(all(feature = "async-client", not(feature = "blocking-client")))] @@ -153,6 +153,59 @@ async fn handshake_v1_and_request() -> crate::Result { Ok(()) } +#[crate::bisync::bisync] +#[cfg_attr(feature = "blocking-client", test)] +#[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] +async fn git_daemon_request_rejects_nul_and_lf() -> crate::Result { + for (control, name) in [(b'\0', "NUL"), (b'\n', "newline")] { + let mut invalid_path = b"/foo.git".to_vec(); + invalid_path.push(control); + let mut invalid_host = b"example.org".to_vec(); + invalid_host.push(control); + let cases: [(bstr::BString, String, &str, &str); 2] = [ + ( + invalid_path.into(), + "example.org".into(), + "path", + "git daemon repository paths must not contain NUL or LF", + ), + ( + "/foo.git".into(), + String::from_utf8(invalid_host).expect("the test host remains UTF-8"), + "host", + "git daemon virtual hosts must not contain NUL or LF", + ), + ]; + + for (path, host, component, expected_error) in cases { + let mut out = Vec::new(); + let server_response = fixture_bytes("v1/clone.response"); + let mut connection = Connection::new( + server_response.as_slice(), + &mut out, + Protocol::V1, + path, + Some((host, None)), + git::ConnectMode::Daemon, + false, + ); + let error = connection + .handshake(Service::UploadPack, &[]) + .await + .err() + .expect("an invalid request must fail"); + assert_eq!( + error.source().expect("the validation error is preserved").to_string(), + expected_error, + "a {name} in the {component} must prevent the request" + ); + drop(connection); + assert!(out.is_empty(), "an invalid request must not be written"); + } + } + Ok(()) +} + #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)]
