Update gix-* crates
Fixes [CVE-2026-0810]. The `gix` crates require updating due to the security vulnerability above. They require updating together in lock-step so both `radicle-fetch` and `radicle-oid` are affected. `radicle-oid` handles the non-exhaustive nature of `ObjectId`. `radicle-fetch` updates to the new API types, however, this comes with some updates to the `ls_refs` protocol. A regression is documented in [gitoxide issue 2429]. The regression highlighted that it is the duty of the caller to also filter the outcome of ls-refs, and `ref-prefix` is simply an optimisation. The ls-refs code is refactored to better represent and handle this operation. [CVE-2026-0810]: https://www.cve.org/CVERecord?id=CVE-2026-0810 [gitoxide issue 2429]: https://github.com/GitoxideLabs/gitoxide/issues/2429
This commit is contained in:
parent
391571178e
commit
7862e108b6
File diff suppressed because it is too large
Load Diff
|
|
@ -30,7 +30,7 @@ cyphernet = "0.5.2"
|
|||
dunce = "1.0.5"
|
||||
fastrand = { version = "2.0.0", default-features = false }
|
||||
git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] }
|
||||
gix-hash = { version = "0.19.0", default-features = false }
|
||||
gix-hash = { version = "0.22.1", default-features = false, features = ["sha1"] }
|
||||
human-panic = "2.0.6"
|
||||
itertools = "0.14"
|
||||
lexopt = "0.3.0"
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ rust-version.workspace = true
|
|||
[dependencies]
|
||||
bstr = { workspace = true }
|
||||
either = "1.9.0"
|
||||
gix-features = { version = "0.43.1", features = ["progress"] }
|
||||
gix-features = { version = "0.46", features = ["progress"] }
|
||||
gix-hash = { workspace = true }
|
||||
gix-odb = "0.70.0"
|
||||
gix-pack = "0.60.0"
|
||||
gix-protocol = { version = "0.51.0", features = ["blocking-client"] }
|
||||
gix-transport = { version = "0.48.0", features = ["blocking-client"] }
|
||||
gix-odb = "0.75.0"
|
||||
gix-pack = "0.65.0"
|
||||
gix-protocol = { version = "0.57.0", features = ["blocking-client"] }
|
||||
gix-refspec = "0.37"
|
||||
gix-transport = { version = "0.54.0", features = ["blocking-client"] }
|
||||
log = { workspace = true, features = ["std"] }
|
||||
nonempty = { workspace = true }
|
||||
radicle = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ mod state;
|
|||
use std::io;
|
||||
use std::time::Instant;
|
||||
|
||||
use gix_protocol::handshake;
|
||||
use gix_protocol::{handshake, Handshake};
|
||||
|
||||
pub use gix_protocol::{transport::bstr::ByteSlice, RemoteProgress};
|
||||
pub use handle::Handle;
|
||||
|
|
@ -131,7 +131,7 @@ where
|
|||
result
|
||||
}
|
||||
|
||||
fn perform_handshake<R, S>(handle: &mut Handle<R, S>) -> Result<handshake::Outcome, Error>
|
||||
fn perform_handshake<R, S>(handle: &mut Handle<R, S>) -> Result<Handshake, Error>
|
||||
where
|
||||
S: transport::ConnectionStream,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
|
||||
use bstr::BString;
|
||||
use bstr::{BStr, BString};
|
||||
use either::Either;
|
||||
use gix_protocol::handshake::Ref;
|
||||
use nonempty::NonEmpty;
|
||||
|
|
@ -123,6 +123,36 @@ impl RefPrefix {
|
|||
RefPrefix::AllNamespaces => "refs/namespaces".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the [`RefPrefix`] into its equivalent [`RefSpec`].
|
||||
///
|
||||
/// See the [`RefPrefix`] variants for their [`BString`] values.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if the reference as a [`BString`] value no longer parses
|
||||
/// in the upstream [`gix_refspec`] crate.
|
||||
///
|
||||
/// [`RefSpec`]: gix_refspec::RefSpec
|
||||
pub fn as_refspec(&self) -> gix_refspec::RefSpec {
|
||||
use gix_refspec::parse::Operation;
|
||||
let parse = |spec: &BStr| -> gix_refspec::RefSpec {
|
||||
gix_refspec::parse(spec, Operation::Fetch)
|
||||
.expect("RefPrefix should be valid refspec")
|
||||
.to_owned()
|
||||
};
|
||||
|
||||
match self {
|
||||
RefPrefix::RadId => parse(refs::REFS_RAD_ID.as_bstr()),
|
||||
RefPrefix::NamespacedRadId { namespace } => {
|
||||
parse(radicle::git::refs::storage::id(namespace).as_bstr())
|
||||
}
|
||||
RefPrefix::NamespacedRadSigrefs { namespace } => {
|
||||
parse(radicle::git::refs::storage::sigrefs(namespace).as_bstr())
|
||||
}
|
||||
RefPrefix::AllNamespaces => parse(BStr::new("refs/namespaces")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`ProtocolStage`] describes a single roundtrip with the Radicle
|
||||
|
|
@ -657,3 +687,26 @@ where
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::RefPrefix;
|
||||
|
||||
/// Ensure that the call to [`RefPrefix::as_refspec`] does not panic
|
||||
#[test]
|
||||
fn valid_refspecs() {
|
||||
let namespace = "z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let prefixes = [
|
||||
RefPrefix::AllNamespaces,
|
||||
RefPrefix::RadId,
|
||||
RefPrefix::NamespacedRadId { namespace },
|
||||
RefPrefix::NamespacedRadSigrefs { namespace },
|
||||
];
|
||||
|
||||
for prefix in prefixes {
|
||||
prefix.as_refspec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time::Instant;
|
||||
|
||||
use gix_protocol::handshake;
|
||||
use gix_protocol::Handshake;
|
||||
use radicle::crypto::PublicKey;
|
||||
use radicle::git::{fmt::Qualified, Oid};
|
||||
use radicle::identity::{Did, Doc, DocError};
|
||||
|
|
@ -214,7 +214,7 @@ impl FetchState {
|
|||
pub(super) fn run_stage<R, S, F>(
|
||||
&mut self,
|
||||
handle: &mut Handle<R, S>,
|
||||
handshake: &handshake::Outcome,
|
||||
handshake: &Handshake,
|
||||
step: &F,
|
||||
) -> Result<BTreeSet<PublicKey>, error::Step>
|
||||
where
|
||||
|
|
@ -231,7 +231,7 @@ impl FetchState {
|
|||
.collect::<Vec<_>>(),
|
||||
None => vec![],
|
||||
};
|
||||
log::trace!("Received refs {refs:?}");
|
||||
log::trace!("Received refs {refs:#?}");
|
||||
step.pre_validate(&refs)?;
|
||||
|
||||
let wants_haves = step.wants_haves(handle.repository(), &refs)?;
|
||||
|
|
@ -288,7 +288,7 @@ impl FetchState {
|
|||
fn run_special_refs<R, S>(
|
||||
&mut self,
|
||||
handle: &mut Handle<R, S>,
|
||||
handshake: &handshake::Outcome,
|
||||
handshake: &Handshake,
|
||||
delegates: BTreeSet<PublicKey>,
|
||||
threshold: usize,
|
||||
limit: &FetchLimit,
|
||||
|
|
@ -357,7 +357,7 @@ impl FetchState {
|
|||
pub(super) fn run<R, S>(
|
||||
mut self,
|
||||
handle: &mut Handle<R, S>,
|
||||
handshake: &handshake::Outcome,
|
||||
handshake: &Handshake,
|
||||
limit: FetchLimit,
|
||||
remote: PublicKey,
|
||||
refs_at: Option<Vec<RefsAt>>,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use std::sync::Arc;
|
|||
use bstr::BString;
|
||||
use gix_features::progress::prodash::progress;
|
||||
use gix_protocol::handshake;
|
||||
use gix_protocol::Handshake;
|
||||
use gix_transport::client;
|
||||
use gix_transport::Protocol;
|
||||
use gix_transport::Service;
|
||||
|
|
@ -91,11 +92,12 @@ where
|
|||
|
||||
/// Perform the handshake with the server side.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn handshake(&mut self) -> Result<handshake::Outcome, handshake::Error> {
|
||||
pub(crate) fn handshake(&mut self) -> Result<Handshake, handshake::Error> {
|
||||
log::trace!("Performing handshake for {}", self.repo);
|
||||
let (read, write) = self.stream.open();
|
||||
gix_protocol::fetch::handshake(
|
||||
gix_protocol::handshake(
|
||||
&mut Connection::new(read, write, self.repo.clone()),
|
||||
Service::UploadPack,
|
||||
|_| Ok(None),
|
||||
vec![],
|
||||
&mut progress::Discard,
|
||||
|
|
@ -106,7 +108,7 @@ where
|
|||
pub(crate) fn ls_refs(
|
||||
&mut self,
|
||||
prefixes: impl IntoIterator<Item = RefPrefix>,
|
||||
handshake: &handshake::Outcome,
|
||||
handshake: &Handshake,
|
||||
) -> Result<Vec<handshake::Ref>, Error> {
|
||||
let prefixes = prefixes.into_iter().collect::<BTreeSet<_>>();
|
||||
let (read, write) = self.stream.open();
|
||||
|
|
@ -126,7 +128,7 @@ where
|
|||
&mut self,
|
||||
wants_haves: WantsHaves,
|
||||
interrupt: Arc<AtomicBool>,
|
||||
handshake: &handshake::Outcome,
|
||||
handshake: &Handshake,
|
||||
) -> Result<Option<Keepfile>, Error> {
|
||||
log::trace!(
|
||||
"Running fetch wants={:?}, haves={:?}",
|
||||
|
|
@ -179,7 +181,7 @@ where
|
|||
}
|
||||
|
||||
pub(crate) struct Connection<R, W> {
|
||||
inner: client::git::Connection<R, W>,
|
||||
inner: client::git::blocking_io::Connection<R, W>,
|
||||
}
|
||||
|
||||
impl<R, W> Connection<R, W>
|
||||
|
|
@ -189,7 +191,7 @@ where
|
|||
{
|
||||
pub fn new(read: R, write: W, repo: BString) -> Self {
|
||||
Self {
|
||||
inner: client::git::Connection::new(
|
||||
inner: client::git::blocking_io::Connection::new(
|
||||
read,
|
||||
write,
|
||||
Protocol::V2,
|
||||
|
|
@ -202,21 +204,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<R, W> client::Transport for Connection<R, W>
|
||||
where
|
||||
R: std::io::Read,
|
||||
W: std::io::Write,
|
||||
{
|
||||
fn handshake<'b>(
|
||||
&mut self,
|
||||
service: Service,
|
||||
extra_parameters: &'b [(&'b str, Option<&'b str>)],
|
||||
) -> Result<client::SetServiceResponse<'_>, client::Error> {
|
||||
self.inner.handshake(service, extra_parameters)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, W> client::TransportWithoutIO for Connection<R, W>
|
||||
impl<R, W> client::blocking_io::Transport for Connection<R, W>
|
||||
where
|
||||
R: std::io::Read,
|
||||
W: std::io::Write,
|
||||
|
|
@ -226,10 +214,24 @@ where
|
|||
write_mode: client::WriteMode,
|
||||
on_into_read: client::MessageKind,
|
||||
trace: bool,
|
||||
) -> Result<client::RequestWriter<'_>, client::Error> {
|
||||
) -> Result<client::blocking_io::RequestWriter<'_>, client::Error> {
|
||||
self.inner.request(write_mode, on_into_read, trace)
|
||||
}
|
||||
|
||||
fn handshake<'b>(
|
||||
&mut self,
|
||||
service: Service,
|
||||
extra_parameters: &'b [(&'b str, Option<&'b str>)],
|
||||
) -> Result<client::blocking_io::SetServiceResponse<'_>, client::Error> {
|
||||
self.inner.handshake(service, extra_parameters)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, W> client::TransportWithoutIO for Connection<R, W>
|
||||
where
|
||||
R: std::io::Read,
|
||||
W: std::io::Write,
|
||||
{
|
||||
fn to_url(&self) -> std::borrow::Cow<'_, bstr::BStr> {
|
||||
self.inner.to_url()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ use std::sync::{atomic::AtomicBool, Arc};
|
|||
|
||||
use gix_features::progress::{DynNestedProgress, NestedProgress};
|
||||
use gix_pack as pack;
|
||||
use gix_protocol::fetch;
|
||||
use gix_protocol::fetch::negotiate::one_round::State;
|
||||
use gix_protocol::handshake;
|
||||
use gix_protocol::handshake::Ref;
|
||||
use gix_protocol::{fetch, Handshake};
|
||||
|
||||
use crate::git::packfile;
|
||||
|
||||
|
|
@ -149,7 +148,7 @@ impl fetch::Negotiate for Negotiate {
|
|||
pub(crate) fn run<P, R, W>(
|
||||
wants_haves: WantsHaves,
|
||||
pack_writer: PackWriter,
|
||||
handshake: &handshake::Outcome,
|
||||
handshake: &Handshake,
|
||||
mut conn: Connection<R, W>,
|
||||
progress: &mut P,
|
||||
) -> Result<FetchOut, Error>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ use std::collections::BTreeSet;
|
|||
use std::io;
|
||||
|
||||
use gix_features::progress::Progress;
|
||||
use gix_protocol::handshake::{self, Ref};
|
||||
use gix_protocol::ls_refs;
|
||||
use gix_protocol::handshake::Ref;
|
||||
use gix_protocol::transport::Protocol;
|
||||
use gix_transport::bstr::{BString, ByteVec};
|
||||
use gix_protocol::{ls_refs, Handshake};
|
||||
use gix_transport::bstr::BString;
|
||||
|
||||
use crate::stage::RefPrefix;
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ pub struct Config {
|
|||
/// the `config`.
|
||||
pub(crate) fn run<R, W>(
|
||||
config: Config,
|
||||
handshake: &handshake::Outcome,
|
||||
handshake: &Handshake,
|
||||
mut conn: Connection<R, W>,
|
||||
progress: &mut impl Progress,
|
||||
) -> Result<Vec<Ref>, ls_refs::Error>
|
||||
|
|
@ -42,7 +42,7 @@ where
|
|||
W: io::Write,
|
||||
{
|
||||
log::trace!("Performing ls-refs: {:?}", config.prefixes);
|
||||
let handshake::Outcome {
|
||||
let Handshake {
|
||||
server_protocol_version: protocol,
|
||||
capabilities,
|
||||
..
|
||||
|
|
@ -54,32 +54,32 @@ where
|
|||
)));
|
||||
}
|
||||
|
||||
let prefixes = config
|
||||
.prefixes
|
||||
.into_iter()
|
||||
.map(|prefix| prefix.into_bstring())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let refs = gix_protocol::ls_refs(
|
||||
&mut conn,
|
||||
capabilities,
|
||||
|_caps, args, features| {
|
||||
for prefix in &prefixes {
|
||||
let mut arg = BString::from("ref-prefix ");
|
||||
arg.push_str(prefix);
|
||||
args.push(arg)
|
||||
}
|
||||
features.push(("agent", Some(Cow::Owned(agent_name()))));
|
||||
Ok(gix_protocol::ls_refs::Action::Continue)
|
||||
let (refspecs, prefixes) = {
|
||||
let n = config.prefixes.len();
|
||||
config.prefixes.into_iter().fold(
|
||||
(Vec::with_capacity(n), Vec::with_capacity(n)),
|
||||
|(mut specs, mut prefixes), prefix| {
|
||||
specs.push(prefix.as_refspec());
|
||||
prefixes.push(prefix.into_bstring());
|
||||
(specs, prefixes)
|
||||
},
|
||||
progress,
|
||||
false, /* trace packetlines */
|
||||
)?;
|
||||
)
|
||||
};
|
||||
|
||||
// Even though we sent `ref-prefix`, listed refs must still be
|
||||
// filtered, since `ref-prefix` is just an optimization hint.
|
||||
// See <https://git-scm.com/docs/protocol-v2#_ls_refs>.
|
||||
let refs = refs
|
||||
log::trace!("ls-refs prefixes: {:#?}", refspecs);
|
||||
|
||||
let ls_refs = gix_protocol::LsRefsCommand::new(
|
||||
Some(&refspecs),
|
||||
capabilities,
|
||||
("agent", Some(Cow::Owned(agent_name()))),
|
||||
);
|
||||
|
||||
// According to [1], in the section on `ls-refs`, we must still filter on
|
||||
// this side, since `ref-prefix` is simply an optimization.
|
||||
//
|
||||
// [1]: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/gitprotocol-v2.html
|
||||
let refs = ls_refs
|
||||
.invoke_blocking(&mut conn, progress, false)?
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
let (refname, _, _) = r.unpack();
|
||||
|
|
@ -87,5 +87,6 @@ where
|
|||
})
|
||||
.collect();
|
||||
|
||||
log::trace!("ls-refs received: {refs:#?}");
|
||||
Ok(refs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ mod gix {
|
|||
fn from(other: Other) -> Self {
|
||||
match other {
|
||||
Other::Sha1(digest) => Self::Sha1(digest),
|
||||
_ => panic!("unexpected SHA variant was returned for `gix_hash::ObjectId`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -383,6 +384,7 @@ mod gix {
|
|||
fn eq(&self, other: &Other) -> bool {
|
||||
match (self, other) {
|
||||
(Oid::Sha1(a), Other::Sha1(b)) => a == b,
|
||||
_ => panic!("unexpected SHA variant was returned for `gix_hash::ObjectId`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue