From b1f30a6a05673c4094d59fda80c695472850d671 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Sep 2026 10:48:49 +0200 Subject: [PATCH 1/4] libgitcore: add `sha1dc` as an optional feature The new Rust crate `sha1dc` (https://crates.io/crates/sha1dc) promises not only type safety but also much better performance compared to the collision-detecting SHA-1 library Git uses at the moment. This performance comes mostly from a SIMD-centric design that relies on features provided by many x86_64 and aarch64 CPUs. Let's optionally use this crate, toggled by the build option `DC_SHA1_RS`. To avoid requiring a shim around the `sha1dc_rs_final()` function just to call `die()` upon a detected collision, pass a pointer to that function to Rust and let it call it directly. This is safe: `die()` is a variadic function, but the Rust code calls it with a simple string without any interpolation required. In a pretty unscientific test on a moderately busy Windows Ryzen 7 machine (UCRT64 GCC 16.2), Rust-backed `git-index-pack.exe` using `sha1dc` 0.1.3 was over three times faster than the C backend by median wall time. Here are the results running five iterations of the `sha1dc` C, Rust v0.1.2, and Rust v0.1.3 backends in randomized, balanced order with `--verify --no-rev-index --threads=1 --object-format=sha1`: SHA1DC backend Median Best of five -------------- ------ ------------ C (default) 32.9s 32.1s Rust 0.1.2 10.9s 10.4s Rust 0.1.3 10.7s 10.5s Version 0.1.3 finished first in four of five triplets, but its median advantage over 0.1.2 was only about 2.4%, and the best 0.1.2 run was faster. In other words, the difference is mostly in the noise. On the same machine, using WSL ("Windows Subsystem for Linux") with the same packfile copied to Linux' ext4 filesystem: SHA1DC backend Median Best of five -------------- ------ ------------ C (default) 23.971s 23.435s Rust 0.1.2 9.123s 8.492s Rust 0.1.3 9.020s 8.491s This is overall faster because of the ext4 vs NTFS performance characteristics, but the same finding holds true: the Rust version of `sha1dc` is dramatically faster. Studying the runs with the Linux perf tools reveals that with the C backend, over 70% of the total time is spent in `git_hash_update()`, with either version of the Rust backend it is around 30%. A comparable test on an M4 Mac yields these results: SHA1DC backend Median Best of five -------------- ------ ------------ C (default) 8.035s 8.009s Rust 0.1.2 4.359s 4.338s Rust 0.1.3 4.361s 4.268s Note that the `sha1dc` crate still requires a significantly newer Rust version than Git's existing Rust support requires: 1.87 instead of 1.63 (https://crates.io/api/v1/crates/sha1dc/0.1.3). Assisted-by: GPT-6 Sol Signed-off-by: Johannes Schindelin --- Cargo.toml | 4 +++ Makefile | 29 +++++++++++++++++++ hash.h | 5 ++++ sha1dc_rs.h | 23 ++++++++++++++++ src/lib.rs | 2 ++ src/sha1dc_rs.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 sha1dc_rs.h create mode 100644 src/sha1dc_rs.rs diff --git a/Cargo.toml b/Cargo.toml index 2f51bf5d5ff5f8..0a953dd481248b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,7 @@ rust-version = "1.49.0" crate-type = ["staticlib"] [dependencies] +sha1dc = { version = "0.1.3", optional = true } + +[features] +sha1dc-rs = ["sha1dc"] diff --git a/Makefile b/Makefile index c649c93c510332..0c321494cf953b 100644 --- a/Makefile +++ b/Makefile @@ -567,6 +567,9 @@ include shared.mak # by the git project to migrate to using sha1collisiondetection as a # submodule. # +# Define DC_SHA1_RS to use the sha1dc Rust crate instead of the default +# C implementation. This requires Rust 1.87 or newer. +# # === SHA-256 backend === # # ==== Security ==== @@ -2137,6 +2140,23 @@ ifdef PPC_SHA1 $(error the PPC_SHA1 flag has been removed along with the PowerPC-specific SHA-1 implementation.) endif +ifdef DC_SHA1_RS +ifdef NO_RUST +$(error DC_SHA1_RS requires Rust support) +endif +ifneq ($(strip $(OPENSSL_SHA1)$(BLK_SHA1)$(APPLE_COMMON_CRYPTO_SHA1)),) +$(error DC_SHA1_RS cannot be combined with another SHA-1 backend) +endif +ifdef DC_SHA1_EXTERNAL +$(error Only set DC_SHA1_RS or DC_SHA1_EXTERNAL, not both) +endif +ifdef DC_SHA1_SUBMODULE +ifneq ($(DC_SHA1_SUBMODULE),auto) +$(error Only set DC_SHA1_RS or DC_SHA1_SUBMODULE, not both) +endif +endif +endif + ifdef OPENSSL_SHA1 EXTLIBS += $(LIB_4_CRYPTO) BASIC_CFLAGS += -DSHA1_OPENSSL @@ -2150,6 +2170,14 @@ ifdef APPLE_COMMON_CRYPTO_SHA1 BASIC_CFLAGS += -DSHA1_APPLE else BASIC_CFLAGS += -DSHA1_DC +ifdef DC_SHA1_RS + BASIC_CFLAGS += -DDC_SHA1_RS + CARGO_ARGS += --features sha1dc-rs + RUST_SOURCES += src/sha1dc_rs.rs +ifeq ($(uname_S),MINGW) + EXTLIBS += -luserenv +endif +else LIB_OBJS += sha1dc_git.o ifdef DC_SHA1_EXTERNAL ifdef DC_SHA1_SUBMODULE @@ -2177,6 +2205,7 @@ endif endif endif endif +endif ifdef OPENSSL_SHA1_UNSAFE ifndef OPENSSL_SHA1 diff --git a/hash.h b/hash.h index cf94ad57002788..dd2e66e1c9ce87 100644 --- a/hash.h +++ b/hash.h @@ -12,8 +12,13 @@ # include "sha1/openssl.h" # endif #elif defined(SHA1_DC) +#ifdef DC_SHA1_RS +#define SHA1_BACKEND "SHA1_DC-rs" +#include "sha1dc_rs.h" +#else #define SHA1_BACKEND "SHA1_DC" #include "sha1dc_git.h" +#endif #else /* SHA1_BLK */ #define SHA1_BACKEND "SHA1_BLK (No collision detection)" #include "block-sha1/sha1.h" diff --git a/sha1dc_rs.h b/sha1dc_rs.h new file mode 100644 index 00000000000000..35e3865d721ff1 --- /dev/null +++ b/sha1dc_rs.h @@ -0,0 +1,23 @@ +#ifndef SHA1DC_RS_H +#define SHA1DC_RS_H + +#define platform_SHA_IS_SHA1DC /* used by "test-tool sha1-is-sha1dc" */ + +typedef struct sha1dc_rs_hasher *SHA1_CTX; + +void sha1dc_rs_init(SHA1_CTX *); +void sha1dc_rs_clone(SHA1_CTX *, const SHA1_CTX *); +void sha1dc_rs_update(SHA1_CTX *, const void *, size_t); +void sha1dc_rs_final(unsigned char [20], SHA1_CTX *, + void (*die_fn)(const char *, ...)); +void sha1dc_rs_discard(SHA1_CTX *); + +#define platform_SHA_CTX SHA1_CTX +#define platform_SHA1_Init sha1dc_rs_init +#define platform_SHA1_Update sha1dc_rs_update +#define platform_SHA1_Final(hash, ctx) sha1dc_rs_final((hash), (ctx), die) +#define SHA1_NEEDS_CLONE_HELPER +#define platform_SHA1_Clone sha1dc_rs_clone +#define platform_SHA1_Discard sha1dc_rs_discard + +#endif diff --git a/src/lib.rs b/src/lib.rs index 0c598298b1c6bf..a34f4d489c20cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ pub mod csum_file; pub mod hash; pub mod loose; +#[cfg(feature = "sha1dc-rs")] +mod sha1dc_rs; pub mod varint; diff --git a/src/sha1dc_rs.rs b/src/sha1dc_rs.rs new file mode 100644 index 00000000000000..df075a5d830898 --- /dev/null +++ b/src/sha1dc_rs.rs @@ -0,0 +1,72 @@ +use sha1dc::Hasher; +use std::ffi::CString; +use std::os::raw::c_char; +use std::{ptr, slice}; + +/// Initialize a collision-detecting SHA-1 context. +/// +/// # Safety +/// `ctx` must point to an uninitialized SHA-1 context. +#[no_mangle] +pub unsafe extern "C" fn sha1dc_rs_init(ctx: *mut *mut Hasher) { + *ctx = Box::into_raw(Box::new(Hasher::new())); +} + +/// Replace a SHA-1 context with a clone of another. +/// +/// # Safety +/// Both contexts must be initialized. +#[no_mangle] +pub unsafe extern "C" fn sha1dc_rs_clone(dst: *mut *mut Hasher, src: *const *mut Hasher) { + let hasher = Box::new((**src).clone()); + drop(Box::from_raw(*dst)); + *dst = Box::into_raw(hasher); +} + +/// Update the SHA-1 hasher with the given bytes. +/// +/// # Safety +/// `ctx` must be initialized and `data` must point to `len` bytes unless +/// `len` is zero. +#[no_mangle] +pub unsafe extern "C" fn sha1dc_rs_update(ctx: *mut *mut Hasher, data: *const c_char, len: usize) { + if len != 0 { + (**ctx).update(slice::from_raw_parts(data.cast::(), len)); + } +} + +/// Finalize SHA-1, reporting detected collisions through `die`. +/// +/// # Safety +/// `ctx` must be initialized, `hash` must point to at least 20 bytes, and +/// `die` must be a non-returning C variadic function. +#[no_mangle] +pub unsafe extern "C" fn sha1dc_rs_final( + hash: *mut u8, + ctx: *mut *mut Hasher, + die: unsafe extern "C" fn(*const c_char, ...) -> !, +) { + let hasher = *Box::from_raw(*ctx); + *ctx = ptr::null_mut(); + match hasher.finalize() { + Ok(digest) => ptr::copy_nonoverlapping(digest.as_bytes().as_ptr(), hash, 20), + Err(collision) => { + let message = CString::new(format!( + "SHA-1 appears to be part of a collision attack: {}", + collision.digest() + )) + .expect("collision message contains no NUL"); + die(message.as_ptr()); + } + } +} + +/// Discard a SHA-1 context without producing a digest. +/// +/// # Safety +/// `ctx` must be initialized. +#[no_mangle] +pub unsafe extern "C" fn sha1dc_rs_discard(ctx: *mut *mut Hasher) { + drop(Box::from_raw(*ctx)); + *ctx = ptr::null_mut(); +} From 5a414a4babf9cb755b4cf43eb42d1f06c8b45d6c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 26 Sep 2026 12:16:35 +0200 Subject: [PATCH 2/4] sha1dc: allow selecting the C backend without rebuilding The Rust `sha1dc` create is really new. While it produced only correct hashes in my hands, before unleashing this to the masses, we need to provide an "escape hatch" in case it doesn't do the right thing. Therefore, when building with `DC_SHA1_RS`, use the Rust `sha1dc` by default, yet also offer to use the C version of `sha1dc` via `core.sha1dcBackend=c` (and `core.sha1dcBackend=rust` to select Rust explicitly). This is made possible by a set of function pointers that are initialized upon the first call to the `git_hash_init()` function. Note that the order in which `hex.h` and `sha1dc_git.h` are included in `sha1dc_git.c` now have to be turned the other way round, to avoid redefining the `platform_SHA*` constants. Assisted-by: GPT-6 Sol Signed-off-by: Johannes Schindelin --- Documentation/config/core.adoc | 5 +++ Makefile | 10 +++--- sha1dc_git.c | 60 +++++++++++++++++++++++++++++++--- sha1dc_git.h | 5 +-- sha1dc_rs.h | 30 ++++++++++++----- src/sha1dc_rs.rs | 18 ++++++---- t/helper/test-sha1.c | 19 ++++++++++- t/t0013-sha1dc.sh | 21 +++++++++++- 8 files changed, 141 insertions(+), 27 deletions(-) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 340329edc38143..2ef56a107e7a9c 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -382,6 +382,11 @@ core.repositoryFormatVersion:: Internal variable identifying the repository format and layout version. See linkgit:gitrepository-layout[5]. +core.sha1dcBackend:: + Select the collision-detecting SHA-1 implementation when Git is built + with `DC_SHA1_RS`: valid values are `rust` (the default) and `c` (the C + fallback). This setting has no effect with other SHA-1 backends. + core.sharedRepository:: When 'group' (or 'true'), the repository is made shareable between several users in a group (making sure all the files and objects are diff --git a/Makefile b/Makefile index 0c321494cf953b..31afa7d9178790 100644 --- a/Makefile +++ b/Makefile @@ -567,8 +567,9 @@ include shared.mak # by the git project to migrate to using sha1collisiondetection as a # submodule. # -# Define DC_SHA1_RS to use the sha1dc Rust crate instead of the default -# C implementation. This requires Rust 1.87 or newer. +# Define DC_SHA1_RS to use the sha1dc Rust crate by default, with the C +# implementation available via core.sha1dcBackend=c. This requires Rust +# 1.87 or newer. # # === SHA-256 backend === # @@ -2170,6 +2171,7 @@ ifdef APPLE_COMMON_CRYPTO_SHA1 BASIC_CFLAGS += -DSHA1_APPLE else BASIC_CFLAGS += -DSHA1_DC + LIB_OBJS += sha1dc_git.o ifdef DC_SHA1_RS BASIC_CFLAGS += -DDC_SHA1_RS CARGO_ARGS += --features sha1dc-rs @@ -2177,8 +2179,7 @@ ifdef DC_SHA1_RS ifeq ($(uname_S),MINGW) EXTLIBS += -luserenv endif -else - LIB_OBJS += sha1dc_git.o +endif ifdef DC_SHA1_EXTERNAL ifdef DC_SHA1_SUBMODULE ifneq ($(DC_SHA1_SUBMODULE),auto) @@ -2205,7 +2206,6 @@ endif endif endif endif -endif ifdef OPENSSL_SHA1_UNSAFE ifndef OPENSSL_SHA1 diff --git a/sha1dc_git.c b/sha1dc_git.c index fe58d7962a30c9..dcc5c1ca8e88bb 100644 --- a/sha1dc_git.c +++ b/sha1dc_git.c @@ -1,6 +1,13 @@ +#ifdef DC_SHA1_RS +#define USE_THE_REPOSITORY_VARIABLE +#endif #include "git-compat-util.h" -#include "sha1dc_git.h" #include "hex.h" +#include "sha1dc_git.h" +#ifdef DC_SHA1_RS +#include "config.h" +#include "repository.h" +#endif #ifdef DC_SHA1_EXTERNAL /* @@ -16,12 +23,13 @@ void git_SHA1DCInit(SHA1_CTX *ctx) /* * Same as SHA1DCFinal, but convert collision attack case into a verbose die(). */ -void git_SHA1DCFinal(unsigned char hash[20], SHA1_CTX *ctx) +void git_SHA1DCFinal(unsigned char hash[20], SHA1_CTX *ctx, + void (*die_fn)(const char *, ...)) { if (!SHA1DCFinal(hash, ctx)) return; - die("SHA-1 appears to be part of a collision attack: %s", - hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1])); + die_fn("SHA-1 appears to be part of a collision attack: %s", + hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1])); } /* @@ -37,3 +45,47 @@ void git_SHA1DCUpdate(SHA1_CTX *ctx, const void *vdata, size_t len) } SHA1DCUpdate(ctx, data, len); } + +#ifdef DC_SHA1_RS +static void sha1dc_c_clone(SHA1_CTX *dst, const SHA1_CTX *src) +{ + *dst = *src; +} + +static void sha1dc_c_discard(SHA1_CTX *ctx UNUSED) +{ + /* The C context owns no resources. */ +} + +/* The first SHA-1 initialization must precede concurrent hashing. */ +static void sha1dc_choose(SHA1_CTX *ctx); + +void (*sha1dc_init)(SHA1_CTX *) = sha1dc_choose; +void (*sha1dc_clone)(SHA1_CTX *, const SHA1_CTX *); +void (*sha1dc_update)(SHA1_CTX *, const void *, size_t); +void (*sha1dc_final)(unsigned char [20], SHA1_CTX *, + void (*die_fn)(const char *, ...)); +void (*sha1dc_discard)(SHA1_CTX *); + +static void sha1dc_choose(SHA1_CTX *ctx) +{ + const char *backend; + int use_c = 0; + + if (!repo_config_get_string_tmp(the_repository, "core.sha1dcbackend", + &backend)) { + if (!strcasecmp(backend, "c")) + use_c = 1; + else if (strcasecmp(backend, "rust")) + die("invalid value for core.sha1dcBackend: '%s'", + backend); + } + + sha1dc_clone = use_c ? sha1dc_c_clone : sha1dc_rs_clone; + sha1dc_update = use_c ? git_SHA1DCUpdate : sha1dc_rs_update; + sha1dc_final = use_c ? git_SHA1DCFinal : sha1dc_rs_final; + sha1dc_discard = use_c ? sha1dc_c_discard : sha1dc_rs_discard; + sha1dc_init = use_c ? git_SHA1DCInit : sha1dc_rs_init; + sha1dc_init(ctx); +} +#endif diff --git a/sha1dc_git.h b/sha1dc_git.h index 0bcf1aa84b7241..4c4aefe3e8c06e 100644 --- a/sha1dc_git.h +++ b/sha1dc_git.h @@ -14,7 +14,8 @@ void git_SHA1DCInit(SHA1_CTX *); #define git_SHA1DCInit SHA1DCInit #endif -void git_SHA1DCFinal(unsigned char [20], SHA1_CTX *); +void git_SHA1DCFinal(unsigned char [20], SHA1_CTX *, + void (*die_fn)(const char *, ...)); void git_SHA1DCUpdate(SHA1_CTX *ctx, const void *data, size_t len); #define platform_SHA_IS_SHA1DC /* used by "test-tool sha1-is-sha1dc" */ @@ -23,5 +24,5 @@ void git_SHA1DCUpdate(SHA1_CTX *ctx, const void *data, size_t len); #define platform_SHA_CTX SHA1_CTX #define platform_SHA1_Init git_SHA1DCInit #define platform_SHA1_Update git_SHA1DCUpdate -#define platform_SHA1_Final git_SHA1DCFinal +#define platform_SHA1_Final(hash, ctx) git_SHA1DCFinal((hash), (ctx), die) #endif diff --git a/sha1dc_rs.h b/sha1dc_rs.h index 35e3865d721ff1..e5d63453633b2b 100644 --- a/sha1dc_rs.h +++ b/sha1dc_rs.h @@ -1,9 +1,15 @@ #ifndef SHA1DC_RS_H #define SHA1DC_RS_H -#define platform_SHA_IS_SHA1DC /* used by "test-tool sha1-is-sha1dc" */ +#define platform_SHA_CTX union sha1dc_ctx +#include "sha1dc_git.h" -typedef struct sha1dc_rs_hasher *SHA1_CTX; +typedef struct sha1dc_rs_hasher *sha1dc_rs_ctx; + +union sha1dc_ctx { + SHA1_CTX c; + sha1dc_rs_ctx rs; +}; void sha1dc_rs_init(SHA1_CTX *); void sha1dc_rs_clone(SHA1_CTX *, const SHA1_CTX *); @@ -12,12 +18,20 @@ void sha1dc_rs_final(unsigned char [20], SHA1_CTX *, void (*die_fn)(const char *, ...)); void sha1dc_rs_discard(SHA1_CTX *); -#define platform_SHA_CTX SHA1_CTX -#define platform_SHA1_Init sha1dc_rs_init -#define platform_SHA1_Update sha1dc_rs_update -#define platform_SHA1_Final(hash, ctx) sha1dc_rs_final((hash), (ctx), die) +extern void (*sha1dc_init)(SHA1_CTX *); +extern void (*sha1dc_clone)(SHA1_CTX *, const SHA1_CTX *); +extern void (*sha1dc_update)(SHA1_CTX *, const void *, size_t); +extern void (*sha1dc_final)(unsigned char [20], SHA1_CTX *, + void (*die_fn)(const char *, ...)); +extern void (*sha1dc_discard)(SHA1_CTX *); + +#define platform_SHA1_Init(ctx) sha1dc_init(&(ctx)->c) +#define platform_SHA1_Update(ctx, data, len) \ + sha1dc_update(&(ctx)->c, (data), (len)) +#define platform_SHA1_Final(hash, ctx) \ + sha1dc_final((hash), &(ctx)->c, die) #define SHA1_NEEDS_CLONE_HELPER -#define platform_SHA1_Clone sha1dc_rs_clone -#define platform_SHA1_Discard sha1dc_rs_discard +#define platform_SHA1_Clone(dst, src) sha1dc_clone(&(dst)->c, &(src)->c) +#define platform_SHA1_Discard(ctx) sha1dc_discard(&(ctx)->c) #endif diff --git a/src/sha1dc_rs.rs b/src/sha1dc_rs.rs index df075a5d830898..b9c430d0dc9617 100644 --- a/src/sha1dc_rs.rs +++ b/src/sha1dc_rs.rs @@ -1,5 +1,5 @@ use sha1dc::Hasher; -use std::ffi::CString; +use std::ffi::{c_void, CString}; use std::os::raw::c_char; use std::{ptr, slice}; @@ -8,7 +8,8 @@ use std::{ptr, slice}; /// # Safety /// `ctx` must point to an uninitialized SHA-1 context. #[no_mangle] -pub unsafe extern "C" fn sha1dc_rs_init(ctx: *mut *mut Hasher) { +pub unsafe extern "C" fn sha1dc_rs_init(ctx: *mut c_void) { + let ctx = ctx.cast::<*mut Hasher>(); *ctx = Box::into_raw(Box::new(Hasher::new())); } @@ -17,7 +18,9 @@ pub unsafe extern "C" fn sha1dc_rs_init(ctx: *mut *mut Hasher) { /// # Safety /// Both contexts must be initialized. #[no_mangle] -pub unsafe extern "C" fn sha1dc_rs_clone(dst: *mut *mut Hasher, src: *const *mut Hasher) { +pub unsafe extern "C" fn sha1dc_rs_clone(dst: *mut c_void, src: *const c_void) { + let dst = dst.cast::<*mut Hasher>(); + let src = src.cast::<*mut Hasher>(); let hasher = Box::new((**src).clone()); drop(Box::from_raw(*dst)); *dst = Box::into_raw(hasher); @@ -29,7 +32,8 @@ pub unsafe extern "C" fn sha1dc_rs_clone(dst: *mut *mut Hasher, src: *const *mut /// `ctx` must be initialized and `data` must point to `len` bytes unless /// `len` is zero. #[no_mangle] -pub unsafe extern "C" fn sha1dc_rs_update(ctx: *mut *mut Hasher, data: *const c_char, len: usize) { +pub unsafe extern "C" fn sha1dc_rs_update(ctx: *mut c_void, data: *const c_void, len: usize) { + let ctx = ctx.cast::<*mut Hasher>(); if len != 0 { (**ctx).update(slice::from_raw_parts(data.cast::(), len)); } @@ -43,9 +47,10 @@ pub unsafe extern "C" fn sha1dc_rs_update(ctx: *mut *mut Hasher, data: *const c_ #[no_mangle] pub unsafe extern "C" fn sha1dc_rs_final( hash: *mut u8, - ctx: *mut *mut Hasher, + ctx: *mut c_void, die: unsafe extern "C" fn(*const c_char, ...) -> !, ) { + let ctx = ctx.cast::<*mut Hasher>(); let hasher = *Box::from_raw(*ctx); *ctx = ptr::null_mut(); match hasher.finalize() { @@ -66,7 +71,8 @@ pub unsafe extern "C" fn sha1dc_rs_final( /// # Safety /// `ctx` must be initialized. #[no_mangle] -pub unsafe extern "C" fn sha1dc_rs_discard(ctx: *mut *mut Hasher) { +pub unsafe extern "C" fn sha1dc_rs_discard(ctx: *mut c_void) { + let ctx = ctx.cast::<*mut Hasher>(); drop(Box::from_raw(*ctx)); *ctx = ptr::null_mut(); } diff --git a/t/helper/test-sha1.c b/t/helper/test-sha1.c index 349540c4df8b6a..827fd2d6d58178 100644 --- a/t/helper/test-sha1.c +++ b/t/helper/test-sha1.c @@ -1,13 +1,30 @@ +#define USE_THE_REPOSITORY_VARIABLE #include "test-tool.h" #include "hash.h" +#include "setup.h" int cmd__sha1(int ac, const char **av) { return cmd_hash_impl(ac, av, GIT_HASH_SHA1, 0); } -int cmd__sha1_is_sha1dc(int argc UNUSED, const char **argv UNUSED) +int cmd__sha1_is_sha1dc(int argc, const char **argv) { +#ifdef DC_SHA1_RS + if (argc == 2 && !strcmp(argv[1], "--backend")) { + git_SHA_CTX ctx; + int nongit; + + setup_git_directory_gently(the_repository, &nongit); + git_SHA1_Init(&ctx); + puts(sha1dc_init == git_SHA1DCInit ? "c" : "rust"); + git_SHA1_Discard(&ctx); + return 0; + } +#else + if (argc == 2 && !strcmp(argv[1], "--backend")) + return 1; +#endif #ifdef platform_SHA_IS_SHA1DC return 0; #endif diff --git a/t/t0013-sha1dc.sh b/t/t0013-sha1dc.sh index 3ea3169d92ff7c..9f6b72f8ef5b1b 100755 --- a/t/t0013-sha1dc.sh +++ b/t/t0013-sha1dc.sh @@ -13,10 +13,29 @@ then test_done fi +test_lazy_prereq SHA1DC_RS ' + test rust = "$(GIT_CONFIG_PARAMETERS="${SQ}core.sha1dcBackend=rust${SQ}" \ + test-tool sha1-is-sha1dc --backend)" +' + test_expect_success 'test-sha1 detects shattered pdf' ' test_must_fail test-tool sha1 <"$TEST_DATA/shattered-1.pdf" 2>err && test_grep collision err && - test_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err + test_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err && + if test_have_prereq SHA1DC_RS + then + test_must_fail env \ + GIT_CONFIG_PARAMETERS="${SQ}core.sha1dcBackend=c${SQ}" \ + test-tool sha1 <"$TEST_DATA/shattered-1.pdf" 2>err && + test_grep collision err && + test_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err + fi +' + +test_expect_success SHA1DC_RS 'select SHA1DC backend via config' ' + test rust = "$(test-tool sha1-is-sha1dc --backend)" && + test_config core.sha1dcBackend c && + test c = "$(test-tool sha1-is-sha1dc --backend)" ' test_done From 2f3077b60a7777b8d4c21551f9a8d02034a49cee Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 26 Sep 2026 22:37:33 +0200 Subject: [PATCH 3/4] pthread: provide `pthread_once()` shims for Windows and for NO_PTHREADS I am about to introduce logic that needs to perform some initialization once, and once only, even if called concurrently. This is a perfect job for `pthread_once()`, but Git's source code currently lacks a Win32 shim. So let's add one! Also provide a trivial shim for `NO_PTHREAD` builds. Assisted-by: GPT-6 Sol Signed-off-by: Johannes Schindelin --- compat/win32/pthread.c | 16 ++++++++++++++++ compat/win32/pthread.h | 5 +++++ thread-utils.h | 16 ++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/compat/win32/pthread.c b/compat/win32/pthread.c index 398caa96029718..5af95edd3bc145 100644 --- a/compat/win32/pthread.c +++ b/compat/win32/pthread.c @@ -60,6 +60,22 @@ pthread_t pthread_self(void) return t; } +static BOOL CALLBACK win32_pthread_once(PINIT_ONCE once UNUSED, + PVOID parameter, + PVOID *context UNUSED) +{ + (*(void (**)(void))parameter)(); + return TRUE; +} + +int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)) +{ + if (!InitOnceExecuteOnce(once_control, win32_pthread_once, + &init_routine, NULL)) + return err_win_to_posix(GetLastError()); + return 0; +} + int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) { if (SleepConditionVariableCS(cond, mutex, INFINITE) == 0) diff --git a/compat/win32/pthread.h b/compat/win32/pthread.h index d80df8d12af2dc..79a6bd9680b9bd 100644 --- a/compat/win32/pthread.h +++ b/compat/win32/pthread.h @@ -26,6 +26,11 @@ static inline int return_0(int i UNUSED) { #define pthread_mutex_lock EnterCriticalSection #define pthread_mutex_unlock LeaveCriticalSection +typedef INIT_ONCE pthread_once_t; +#define PTHREAD_ONCE_INIT INIT_ONCE_STATIC_INIT + +int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); + typedef int pthread_mutexattr_t; #define pthread_mutexattr_init(a) (*(a) = 0) #define pthread_mutexattr_destroy(a) do {} while (0) diff --git a/thread-utils.h b/thread-utils.h index 4961487ed914f4..98b574eff41e64 100644 --- a/thread-utils.h +++ b/thread-utils.h @@ -19,6 +19,22 @@ #define pthread_mutex_t int #define pthread_cond_t int #define pthread_key_t int +#define pthread_once_t int +#undef PTHREAD_ONCE_INIT +#define PTHREAD_ONCE_INIT 0 + +static inline int dummy_pthread_once(pthread_once_t *once_control, + void (*init_routine)(void)) +{ + if (!*once_control) { + init_routine(); + *once_control = 1; + } + return 0; +} + +#define pthread_once(once_control, init_routine) \ + dummy_pthread_once((once_control), (init_routine)) #define pthread_mutex_init(mutex, attr) dummy_pthread_init(mutex) #define pthread_mutex_lock(mutex) From aac6a83a8ebd91f33cbe2074245b6d45cb264105 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 26 Sep 2026 22:42:01 +0200 Subject: [PATCH 4/4] sha1dc: make `sha1dc_init()` thread-safe The `sha1dc_init` function pointer initially points to a function that determines which sha1dc backend to use. Naturally, this initialization should only run once. To allow for that function to be called concurrently in multiple threads, we need to use a pthread primitive to ensure that the `sha1dc_*()` function pointers are initialized exactly once. Unfortunately, this requires quite a bit of non-DRY code to prevent data races when different threads run `initial_init()` concurrently (see https://en.cppreference.com/c/language/memory_model#Threads_and_data_races). Signed-off-by: Johannes Schindelin --- sha1dc_git.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/sha1dc_git.c b/sha1dc_git.c index dcc5c1ca8e88bb..eba62b12abb055 100644 --- a/sha1dc_git.c +++ b/sha1dc_git.c @@ -7,6 +7,7 @@ #ifdef DC_SHA1_RS #include "config.h" #include "repository.h" +#include "thread-utils.h" #endif #ifdef DC_SHA1_EXTERNAL @@ -58,16 +59,21 @@ static void sha1dc_c_discard(SHA1_CTX *ctx UNUSED) } /* The first SHA-1 initialization must precede concurrent hashing. */ -static void sha1dc_choose(SHA1_CTX *ctx); +static void initial_init(SHA1_CTX *); +static void initial_clone(SHA1_CTX *, const SHA1_CTX *); +static void initial_update(SHA1_CTX *, const void *, size_t); +static void initial_final(unsigned char [20], SHA1_CTX *, + void (*die_fn)(const char *, ...)); +static void initial_discard(SHA1_CTX *ctx); -void (*sha1dc_init)(SHA1_CTX *) = sha1dc_choose; -void (*sha1dc_clone)(SHA1_CTX *, const SHA1_CTX *); -void (*sha1dc_update)(SHA1_CTX *, const void *, size_t); +void (*sha1dc_init)(SHA1_CTX *) = initial_init; +void (*sha1dc_clone)(SHA1_CTX *, const SHA1_CTX *) = initial_clone; +void (*sha1dc_update)(SHA1_CTX *, const void *, size_t) = initial_update; void (*sha1dc_final)(unsigned char [20], SHA1_CTX *, - void (*die_fn)(const char *, ...)); -void (*sha1dc_discard)(SHA1_CTX *); + void (*die_fn)(const char *, ...)) = initial_final; +void (*sha1dc_discard)(SHA1_CTX *) = initial_discard; -static void sha1dc_choose(SHA1_CTX *ctx) +static void sha1dc_choose(void) { const char *backend; int use_c = 0; @@ -86,6 +92,49 @@ static void sha1dc_choose(SHA1_CTX *ctx) sha1dc_final = use_c ? git_SHA1DCFinal : sha1dc_rs_final; sha1dc_discard = use_c ? sha1dc_c_discard : sha1dc_rs_discard; sha1dc_init = use_c ? git_SHA1DCInit : sha1dc_rs_init; +} + +static pthread_once_t once = PTHREAD_ONCE_INIT; + +static void initial_init(SHA1_CTX *ctx) +{ + int ret = pthread_once(&once, sha1dc_choose); + if (ret) + die("cannot initialize SHA-1 backend: %s", strerror(ret)); sha1dc_init(ctx); } + +static void initial_clone(SHA1_CTX *dst, const SHA1_CTX *src) +{ + int ret = pthread_once(&once, sha1dc_choose); + if (ret) + die("cannot initialize SHA-1 backend: %s", strerror(ret)); + sha1dc_clone(dst, src); +} + +static void initial_update(SHA1_CTX *ctx, const void *buf, size_t len) +{ + int ret = pthread_once(&once, sha1dc_choose); + if (ret) + die("cannot initialize SHA-1 backend: %s", strerror(ret)); + sha1dc_update(ctx, buf, len); +} + +static void initial_final(unsigned char hash[20], SHA1_CTX *ctx, + void (*die_fn)(const char *, ...)) +{ + int ret = pthread_once(&once, sha1dc_choose); + if (ret) + die("cannot initialize SHA-1 backend: %s", strerror(ret)); + sha1dc_final(hash, ctx, die_fn); +} + +static void initial_discard(SHA1_CTX *ctx) +{ + int ret = pthread_once(&once, sha1dc_choose); + if (ret) + die("cannot initialize SHA-1 backend: %s", strerror(ret)); + sha1dc_discard(ctx); +} + #endif