From 1affb8b19346c4f90e163a9a0364959ff1410f64 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 4 May 2026 22:25:59 +0100 Subject: [PATCH] Final final v1.9.3 updates (#7250) --- ci/test.sh | 13 +- docs/changelog.md | 12 ++ src/libgit2/index.c | 8 +- src/libgit2/index.h | 3 + src/libgit2/indexer.c | 4 +- src/libgit2/pack-objects.c | 2 +- src/libgit2/streams/socket.c | 30 +++ src/libgit2/submodule.c | 17 +- src/libgit2/transports/httpclient.c | 19 +- src/libgit2/transports/ssh_libssh2.c | 6 +- tests/libgit2/merge/workdir/submodules.c | 10 + tests/libgit2/network/remote/local.c | 15 +- tests/libgit2/online/ssh_timeout.c | 254 +++++++++++++++++++++++ 13 files changed, 360 insertions(+), 33 deletions(-) create mode 100644 tests/libgit2/online/ssh_timeout.c diff --git a/ci/test.sh b/ci/test.sh index 98093c6ec..19d466a65 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -198,7 +198,7 @@ if should_run "PROXY_TESTS"; then fi if should_run "NTLM_TESTS" || should_run "ONLINE_TESTS"; then - curl --location --silent --show-error https://github.com/ethomson/poxygit/releases/download/v0.6.0/poxygit-0.6.0.jar >poxygit.jar + curl --location --silent --show-error https://github.com/ethomson/poxygit/releases/download/v0.8.1/poxygit-0.8.1.jar >poxygit.jar echo "Starting HTTP server..." HTTP_DIR=`mktemp -d ${TMPDIR}/http.XXXXXXXX` @@ -216,18 +216,13 @@ if should_run "SSH_TESTS"; then cat >"${SSHD_DIR}/sshd_config" <<-EOF Port 2222 ListenAddress 0.0.0.0 - Protocol 2 HostKey ${SSHD_DIR}/id_${GITTEST_SSH_KEYTYPE} PidFile ${SSHD_DIR}/pid AuthorizedKeysFile ${HOME}/.ssh/authorized_keys LogLevel DEBUG - RSAAuthentication yes PasswordAuthentication yes PubkeyAuthentication yes - ChallengeResponseAuthentication no StrictModes no - HostCertificate ${SSHD_DIR}/id_${GITTEST_SSH_KEYTYPE}.pub - HostKey ${SSHD_DIR}/id_${GITTEST_SSH_KEYTYPE} # Required here as sshd will simply close connection otherwise UsePAM no EOF @@ -304,10 +299,10 @@ if should_run "ONLINE_TESTS"; then echo "## Running networking (online) tests" echo "##############################################################################" - export GITTEST_REMOTE_REDIRECT_INITIAL="http://localhost:9000/initial-redirect/libgit2/TestGitRepository" + export GITTEST_REMOTE_REDIRECT_INITIAL="http://localhost:9000/initial-redirect:none/libgit2/TestGitRepository" export GITTEST_REMOTE_REDIRECT_SUBSEQUENT="http://localhost:9000/subsequent-redirect/libgit2/TestGitRepository" - export GITTEST_REMOTE_SPEED_SLOW="http://localhost:9000/speed-9600/test.git" - export GITTEST_REMOTE_SPEED_TIMESOUT="http://localhost:9000/speed-0.5/test.git" + export GITTEST_REMOTE_SPEED_SLOW="http://localhost:9000/speed:9600/test.git" + export GITTEST_REMOTE_SPEED_TIMESOUT="http://localhost:9000/speed:0.5/test.git" run_test online unset GITTEST_REMOTE_REDIRECT_INITIAL unset GITTEST_REMOTE_REDIRECT_SUBSEQUENT diff --git a/docs/changelog.md b/docs/changelog.md index 688bd078d..0615565a4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,18 @@ v1.9.3 This release includes a number of bugfixes and compatibility improvements, particularly around SHA256 support. +* cmake: fix linker error when using ninja build generator by + @kcsaul in https://github.com/libgit2/libgit2/pull/7249 +* Handle redirects with Content-Length: 0 correctly by + @ethomson in https://github.com/libgit2/libgit2/pull/7246 +* ci: use poxygit v0.8.1 in the tests by @ethomson in + https://github.com/libgit2/libgit2/pull/7248 +* Zero indexer stats in pack objects by @ethomson in + https://github.com/libgit2/libgit2/pull/7243 +* submodule: git_index_add_bypath does not move conflict entries + to REUC by @lrm29 in https://github.com/libgit2/libgit2/pull/7003 +* fix: prevent SSH timeout infinite loop and enable TCP keepalive + by @ambv in https://github.com/libgit2/libgit2/pull/7165 * merge_files: avoid UB in xdiff by @ethomson in https://github.com/libgit2/libgit2/pull/7239 * git_merge_file_from_index: handle cases when a child (ours or diff --git a/src/libgit2/index.c b/src/libgit2/index.c index 20e9779a3..2ea8f0c4b 100644 --- a/src/libgit2/index.c +++ b/src/libgit2/index.c @@ -1448,7 +1448,7 @@ out: return error; } -static int index_conflict_to_reuc(git_index *index, const char *path) +int git_index__conflict_to_reuc(git_index *index, const char *path) { const git_index_entry *conflict_entries[3]; int ancestor_mode, our_mode, their_mode; @@ -1528,7 +1528,7 @@ int git_index_add_from_buffer( return error; /* Adding implies conflict was resolved, move conflict entries to REUC */ - if ((error = index_conflict_to_reuc(index, entry->path)) < 0 && error != GIT_ENOTFOUND) + if ((error = git_index__conflict_to_reuc(index, entry->path)) < 0 && error != GIT_ENOTFOUND) return error; git_tree_cache_invalidate_path(index->tree, entry->path); @@ -1624,7 +1624,7 @@ int git_index_add_bypath(git_index *index, const char *path) } /* Adding implies conflict was resolved, move conflict entries to REUC */ - if ((ret = index_conflict_to_reuc(index, path)) < 0 && ret != GIT_ENOTFOUND) + if ((ret = git_index__conflict_to_reuc(index, path)) < 0 && ret != GIT_ENOTFOUND) return ret; git_tree_cache_invalidate_path(index->tree, entry->path); @@ -1640,7 +1640,7 @@ int git_index_remove_bypath(git_index *index, const char *path) if (((ret = git_index_remove(index, path, 0)) < 0 && ret != GIT_ENOTFOUND) || - ((ret = index_conflict_to_reuc(index, path)) < 0 && + ((ret = git_index__conflict_to_reuc(index, path)) < 0 && ret != GIT_ENOTFOUND)) return ret; diff --git a/src/libgit2/index.h b/src/libgit2/index.h index 601e98f1c..de8a9cf38 100644 --- a/src/libgit2/index.h +++ b/src/libgit2/index.h @@ -154,6 +154,9 @@ extern int git_index__open( const char *index_path, git_oid_t oid_type); +/* If the path is conflicted, move it from the index to reuc. */ +int git_index__conflict_to_reuc(git_index *index, const char *path); + /* Copy the current entries vector *and* increment the index refcount. * Call `git_index__release_snapshot` when done. */ diff --git a/src/libgit2/indexer.c b/src/libgit2/indexer.c index e62daacfa..915f63fd4 100644 --- a/src/libgit2/indexer.c +++ b/src/libgit2/indexer.c @@ -921,12 +921,12 @@ int git_indexer_append(git_indexer *idx, const void *data, size_t size, git_inde if (git_vector_init(&idx->deltas, total_objects / 2, NULL) < 0) return -1; + stats->total_objects = total_objects; + stats->indexed_objects = 0; stats->received_objects = 0; stats->local_objects = 0; stats->total_deltas = 0; stats->indexed_deltas = 0; - stats->indexed_objects = 0; - stats->total_objects = total_objects; if ((error = do_progress_callback(idx, stats)) != 0) return error; diff --git a/src/libgit2/pack-objects.c b/src/libgit2/pack-objects.c index b015c49e3..489af132f 100644 --- a/src/libgit2/pack-objects.c +++ b/src/libgit2/pack-objects.c @@ -1420,7 +1420,7 @@ int git_packbuilder_write( git_str object_path = GIT_STR_INIT; git_indexer_options opts = GIT_INDEXER_OPTIONS_INIT; git_indexer *indexer = NULL; - git_indexer_progress stats; + git_indexer_progress stats = { 0 }; struct pack_write_context ctx; int t; diff --git a/src/libgit2/streams/socket.c b/src/libgit2/streams/socket.c index a463312fd..d064f867e 100644 --- a/src/libgit2/streams/socket.c +++ b/src/libgit2/streams/socket.c @@ -20,6 +20,10 @@ # include # include # include +# include +# ifdef __APPLE__ +# include +# endif #else # include # include @@ -188,6 +192,32 @@ static int socket_connect(git_stream *stream) for (p = info; p != NULL; p = p->ai_next) { s = socket(p->ai_family, p->ai_socktype | SOCK_CLOEXEC, p->ai_protocol); + /* Enable TCP keepalive to detect dead connections */ + if (s != INVALID_SOCKET && p->ai_family == AF_INET) { + int keepalive = 1; + if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, + (const char *)&keepalive, sizeof(keepalive)) == 0) { +#ifdef __APPLE__ + /* macOS: Set idle time to 60 seconds */ + int keepidle = 60; + setsockopt(s, IPPROTO_TCP, TCP_KEEPALIVE, + &keepidle, sizeof(keepidle)); + /* Note: TCP_CONNECTIONTIMEOUT (0x20) could also be set */ +#elif defined(__linux__) + /* Linux: Set idle, interval, and count */ + int keepidle = 60; /* Start probes after 60 seconds */ + int keepintvl = 10; /* 10 seconds between probes */ + int keepcnt = 3; /* 3 probes before giving up */ + setsockopt(s, IPPROTO_TCP, TCP_KEEPIDLE, + &keepidle, sizeof(keepidle)); + setsockopt(s, IPPROTO_TCP, TCP_KEEPINTVL, + &keepintvl, sizeof(keepintvl)); + setsockopt(s, IPPROTO_TCP, TCP_KEEPCNT, + &keepcnt, sizeof(keepcnt)); +#endif + } + } + if (s == INVALID_SOCKET) continue; diff --git a/src/libgit2/submodule.c b/src/libgit2/submodule.c index a3bfc7872..7940f7001 100644 --- a/src/libgit2/submodule.c +++ b/src/libgit2/submodule.c @@ -1075,16 +1075,23 @@ int git_submodule_add_to_index(git_submodule *sm, int write_index) git_commit_free(head); /* add it */ - error = git_index_add(index, &entry); + if ((error = git_index_add(index, &entry)) < 0) + goto cleanup; + + /* Adding implies conflict was resolved, move conflict entries to REUC */ + if ((error = git_index__conflict_to_reuc(index, entry.path)) < 0 && error != GIT_ENOTFOUND) + goto cleanup; /* write it, if requested */ - if (!error && write_index) { - error = git_index_write(index); + if (write_index) { + if ((error = git_index_write(index)) < 0) + goto cleanup; - if (!error) - git_oid_cpy(&sm->index_oid, &sm->wd_oid); + git_oid_cpy(&sm->index_oid, &sm->wd_oid); } + error = 0; + cleanup: git_repository_free(sm_repo); git_str_dispose(&path); diff --git a/src/libgit2/transports/httpclient.c b/src/libgit2/transports/httpclient.c index e27d40b5f..86bae396e 100644 --- a/src/libgit2/transports/httpclient.c +++ b/src/libgit2/transports/httpclient.c @@ -379,7 +379,7 @@ static int on_headers_complete(git_http_parser *parser) ctx->response->resend_credentials = resend_needed(ctx->client, ctx->response); - if (ctx->response->content_type || ctx->response->chunked) + if (ctx->response->content_length || ctx->response->chunked) ctx->client->state = READING_BODY; else ctx->client->state = DONE; @@ -1243,13 +1243,16 @@ GIT_INLINE(int) client_read_and_parse(git_http_client *client) } /* - * See if we've consumed the entire response body. If the client was - * reading the body but did not consume it entirely, it's possible that - * they knew that the stream had finished (in a git response, seeing a - * final flush) and stopped reading. But if the response was chunked, - * we may have not consumed the final chunk marker. Consume it to - * ensure that we don't have it waiting in our socket. If there's - * more than just a chunk marker, close the connection. + * Try to consume any remaining response body. The client may have + * decided that it did not need to consume the entire response body. + * For example, the client saw a redirect in the header and ignored + * the body. Or the client saw a particular sequence (like a final + * flush in a git response) and stopped reading (but there were + * additional response bytes, perhaps because the response was chunked). + * Do one more read to try to clear this out; this takes care of small + * remainders, like a chunk response or a small redirect message. If + * there is too much data, we'll just leave it and close the + * connection. */ static void complete_response_body(git_http_client *client) { diff --git a/src/libgit2/transports/ssh_libssh2.c b/src/libgit2/transports/ssh_libssh2.c index 6469c8d64..f50774bd2 100644 --- a/src/libgit2/transports/ssh_libssh2.c +++ b/src/libgit2/transports/ssh_libssh2.c @@ -366,7 +366,7 @@ static int _git_ssh_authenticate_session( default: rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; } - } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + } while (LIBSSH2_ERROR_EAGAIN == rc); if (rc == LIBSSH2_ERROR_PASSWORD_EXPIRED || rc == LIBSSH2_ERROR_AUTHENTICATION_FAILED || @@ -556,7 +556,7 @@ static int _git_ssh_session_create( if (git_str_len(&prefs) > 0) { do { rc = libssh2_session_method_pref(s, LIBSSH2_METHOD_HOSTKEY, git_str_cstr(&prefs)); - } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + } while (LIBSSH2_ERROR_EAGAIN == rc); if (rc != LIBSSH2_ERROR_NONE) { ssh_error(s, "failed to set hostkey preference"); goto on_error; @@ -566,7 +566,7 @@ static int _git_ssh_session_create( do { rc = libssh2_session_handshake(s, socket->s); - } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + } while (LIBSSH2_ERROR_EAGAIN == rc); if (rc != LIBSSH2_ERROR_NONE) { ssh_error(s, "failed to start SSH session"); diff --git a/tests/libgit2/merge/workdir/submodules.c b/tests/libgit2/merge/workdir/submodules.c index 5117be789..3938a2d8f 100644 --- a/tests/libgit2/merge/workdir/submodules.c +++ b/tests/libgit2/merge/workdir/submodules.c @@ -53,6 +53,16 @@ void test_merge_workdir_submodules__automerge(void) cl_git_pass(git_repository_index(&index, repo)); cl_assert(merge_test_index(index, merge_index_entries, 6)); + cl_assert_equal_i(true, git_index_has_conflicts(index)); + + /* Put an actual Git repository into the submodule path on disk. + * Add it to the index and assert that the conflict is resolved. + */ + cl_fixture_sandbox("testrepo"); + p_rename("testrepo", TEST_REPO_PATH "/submodule"); + p_rename(TEST_REPO_PATH "/submodule/.gitted", TEST_REPO_PATH "/submodule/.git"); + cl_git_pass(git_index_add_bypath(index, "submodule")); + cl_assert_equal_i(false, git_index_has_conflicts(index)); git_index_free(index); git_annotated_commit_free(their_head); diff --git a/tests/libgit2/network/remote/local.c b/tests/libgit2/network/remote/local.c index d5641398c..ab2f5ad2d 100644 --- a/tests/libgit2/network/remote/local.c +++ b/tests/libgit2/network/remote/local.c @@ -15,6 +15,17 @@ static git_strarray push_array = { 1, }; +static int push_transfer_progress_cb(unsigned int current, unsigned int total, size_t bytes, void* payload) +{ + GIT_UNUSED(current); + GIT_UNUSED(total); + GIT_UNUSED(payload); + + cl_assert(bytes == 0); + + return 0; +} + void test_network_remote_local__initialize(void) { cl_git_pass(git_repository_init(&repo, "remotelocal/", 0)); @@ -200,6 +211,7 @@ void test_network_remote_local__push_to_bare_remote(void) /* Should be able to push to a bare remote */ git_remote *localremote; + git_push_options opts = GIT_PUSH_OPTIONS_INIT; /* Get some commits */ connect_to_local_repository(cl_fixture("testrepo.git")); @@ -217,7 +229,8 @@ void test_network_remote_local__push_to_bare_remote(void) cl_git_pass(git_remote_connect(localremote, GIT_DIRECTION_PUSH, NULL, NULL, NULL)); /* Try to push */ - cl_git_pass(git_remote_upload(localremote, &push_array, NULL)); + opts.callbacks.push_transfer_progress = push_transfer_progress_cb; + cl_git_pass(git_remote_upload(localremote, &push_array, &opts)); /* Clean up */ git_remote_free(localremote); diff --git a/tests/libgit2/online/ssh_timeout.c b/tests/libgit2/online/ssh_timeout.c new file mode 100644 index 000000000..109850bc2 --- /dev/null +++ b/tests/libgit2/online/ssh_timeout.c @@ -0,0 +1,254 @@ +#include "clar_libgit2.h" +#include "git2/sys/transport.h" +#include "thread.h" + +#ifndef _WIN32 +# include +# include +# include +# include +# include +# include +#else +# include +# include +#endif + +extern int git_socket_stream__timeout; + +#ifdef GIT_SSH_LIBSSH2 + +#ifdef _WIN32 +static SOCKET server_socket = INVALID_SOCKET; +#else +static int server_socket = -1; +#endif +static int server_port = 0; +#ifndef _WIN32 +static pthread_t server_thread; +#else +static HANDLE server_thread; +#endif +static git_atomic32 server_running; + +/* Black hole server: accepts connections but never responds */ +#ifdef _WIN32 +static DWORD WINAPI blackhole_server(LPVOID param) +{ + SOCKET client_socket; + struct sockaddr_in client_addr; + int client_len = sizeof(client_addr); + + GIT_UNUSED(param); + + git_atomic32_set(&server_running, 1); + + while (git_atomic32_get(&server_running)) { + client_socket = accept(server_socket, + (struct sockaddr *)&client_addr, &client_len); + if (client_socket == INVALID_SOCKET) + break; + + /* Accept the connection but never send data - this will + * cause SSH handshake to timeout */ + Sleep(10000); /* 10 seconds */ + closesocket(client_socket); + } + + return 0; +} +#else +static void *blackhole_server(void *param) +{ + int client_socket; + struct sockaddr_in client_addr; + socklen_t client_len = sizeof(client_addr); + + GIT_UNUSED(param); + + git_atomic32_set(&server_running, 1); + + while (git_atomic32_get(&server_running)) { + client_socket = accept(server_socket, + (struct sockaddr *)&client_addr, &client_len); + if (client_socket < 0) + break; + + /* Accept the connection but never send data - this will + * cause SSH handshake to timeout */ + sleep(10); /* 10 seconds */ + close(client_socket); + } + + return NULL; +} +#endif + +static int start_blackhole_server(void) +{ + struct sockaddr_in addr; +#ifdef _WIN32 + int addr_len = sizeof(addr); +#else + socklen_t addr_len = sizeof(addr); +#endif + int opt = 1; + +#ifdef _WIN32 + WSADATA wsa_data; + WSAStartup(MAKEWORD(2, 2), &wsa_data); +#endif + + server_socket = socket(AF_INET, SOCK_STREAM, 0); +#ifdef _WIN32 + if (server_socket == INVALID_SOCKET) + return -1; +#else + if (server_socket < 0) + return -1; +#endif + +#ifdef _WIN32 + setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, + (const char *)&opt, sizeof(opt)); +#else + setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, + &opt, sizeof(opt)); +#endif + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + addr.sin_port = 0; /* Let OS choose port */ + + if (bind(server_socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) { +#ifdef _WIN32 + closesocket(server_socket); +#else + close(server_socket); +#endif + return -1; + } + + if (listen(server_socket, 5) < 0) { +#ifdef _WIN32 + closesocket(server_socket); +#else + close(server_socket); +#endif + return -1; + } + + /* Get the actual port assigned */ + if (getsockname(server_socket, (struct sockaddr *)&addr, &addr_len) < 0) { +#ifdef _WIN32 + closesocket(server_socket); +#else + close(server_socket); +#endif + return -1; + } + server_port = ntohs(addr.sin_port); + + /* Start server thread */ +#ifdef _WIN32 + server_thread = CreateThread(NULL, 0, blackhole_server, NULL, 0, NULL); + if (server_thread == NULL) { + closesocket(server_socket); + return -1; + } +#else + if (pthread_create(&server_thread, NULL, blackhole_server, NULL) != 0) { + close(server_socket); + return -1; + } +#endif + + return 0; +} + +static void stop_blackhole_server(void) +{ + git_atomic32_set(&server_running, 0); + +#ifdef _WIN32 + if (server_socket != INVALID_SOCKET) { + closesocket(server_socket); + if (server_thread) + WaitForSingleObject(server_thread, INFINITE); + server_socket = INVALID_SOCKET; + } + WSACleanup(); +#else + if (server_socket >= 0) { + close(server_socket); + pthread_join(server_thread, NULL); + server_socket = -1; + } +#endif +} + +#endif /* GIT_SSH_LIBSSH2 */ + +/* + * Test that SSH connection timeout doesn't cause infinite retry loop. + * + * This test creates a TCP server that accepts connections but never + * responds to SSH handshake, causing libssh2 to timeout. + * + * Before the fix: The code would retry indefinitely on LIBSSH2_ERROR_TIMEOUT + * After the fix: The code properly returns an error after first timeout + */ +void test_online_ssh_timeout__no_infinite_loop(void) +{ +#ifndef GIT_SSH_LIBSSH2 + cl_skip(); +#else + git_remote *remote = NULL; + git_repository *repo = NULL; + git_transport *transport = NULL; + git_remote_connect_options opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + char url[256]; + int old_timeout; + clock_t start, end; + double elapsed_ms; + + /* Start black hole server */ + cl_git_pass(start_blackhole_server()); + + /* Create URL to our black hole server */ + sprintf(url, "ssh://localhost:%d/test.git", server_port); + + /* Set a short timeout (100ms) */ + old_timeout = git_socket_stream__timeout; + git_socket_stream__timeout = 100; + + cl_git_pass(git_repository_init(&repo, "./transport-timeout", 0)); + cl_git_pass(git_remote_create(&remote, repo, "test", url)); + + /* Get transport */ + cl_git_pass(git_transport_new(&transport, remote, url)); + + /* Attempt connection - should fail due to timeout */ + start = clock(); + cl_git_fail(transport->connect(transport, url, + GIT_SERVICE_UPLOADPACK_LS, &opts)); + end = clock(); + + /* Calculate elapsed time in milliseconds */ + elapsed_ms = ((double)(end - start) / CLOCKS_PER_SEC) * 1000.0; + + /* With the fix, this should fail relatively quickly (within 2 seconds). + * Without the fix, it would loop many times and take much longer. + * We use a generous timeout of 5 seconds to avoid flakiness. */ + cl_assert(elapsed_ms < 5000); + + /* Cleanup */ + transport->free(transport); + git_remote_free(remote); + git_repository_free(repo); + git_socket_stream__timeout = old_timeout; + + stop_blackhole_server(); +#endif +}