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)]

Reply via email to