Package: csync2
Version: 2.0-42-g83b3644-3
Severity: important
Tags: patch upstream

Hi,

Following up on our mail exchange here is the report you asked for.

On a GnuTLS build, a large synchronisation never finishes in one run. It aborts after roughly 7.4 GiB moved over a single connection, with

    Read-error while receiving data.      # receiving node
    Write-error while sending data.       # sending node

and resumes where it stopped on the next run. Pushing ~31 GB / 56k files to two peers took seven runs of csync2 to converge.

What ruled out a flaky link is that the abort point is deterministic: 8.0 GB, then 15.4 GB, then 22.9 GB transferred, the same figures on two different peers
and again after rebuilding the package from source.

I patched READ() and WRITE() in conn.c to log what GnuTLS actually returns before csync2 gives up. Both ends report the same thing:

    sender:   rc=-28 (Resource temporarily unavailable, try again.) count=512     receiver: rc=-28 (Resource temporarily unavailable, try again.) fatal=0 count=512

-28 is GNUTLS_E_AGAIN, and gnutls_error_is_fatal() returns 0 for it. csync2 passes the negative return up the stack anyway: conn_read() turns it into a short read and csync_recv_file() calls csync_fatal(), while on the other side csync_send_file() sees rc != chunk and does the same.

Worth noting because it cost me a test run: patching only the sending side changes nothing at all,  same seven runs, same thresholds,  because the receive
path then aborts on the identical condition. Both need fixing.

With both patched, the same 31.66 GB to two peers goes through in a single run, 1938 s, no aborts. I verified the result each time by comparing an md5 digest of the whole tree on all three nodes.

The patch is attached (DEP-3 header included). It applies to the packaging source as-is, and to current upstream master (253bfc48) with offsets of +3 and
-4 lines.

Reported upstream as https://github.com/LINBIT/csync2/issues/45. Another user has since confirmed there that the bug matches long-standing trouble they had
with large files, and that building with the patch fixes it for them.

Also worth mentioning while you are looking at the package: the security tracker still lists trixie as open for CVE-2026-41051, fixed from -6 onwards.

Happy to test anything else, I still have the three-node setup.

Regards,
Description: Retry TLS reads and writes that return GNUTLS_E_AGAIN
 gnutls_record_send() and gnutls_record_recv() may return GNUTLS_E_AGAIN or
 GNUTLS_E_INTERRUPTED. Both are non-fatal -- gnutls_error_is_fatal() returns 0
 for them -- and the call is meant to be repeated. csync2 passes the negative
 return straight up: conn_read() turns it into a short read and
 csync_recv_file() calls csync_fatal("Read-error while receiving data."), while
 csync_send_file() sees rc != chunk and calls csync_fatal("Write-error while
 sending data.").
 .
 The whole transfer therefore dies on a condition GnuTLS reports as harmless.
 In practice this happens after roughly 7.4 GiB moved over a single connection,
 which makes a large first synchronisation abort and restart repeatedly until
 it eventually completes. Instrumenting both call sites on a three-node cluster
 shows the cause directly:
 .
   sender:   TLSDIAG send rc=-28 (Resource temporarily unavailable, try again.) count=512
   receiver: TLSDIAG recv rc=-28 (Resource temporarily unavailable, try again.) fatal=0 count=512
 .
 Both ends must be fixed. Patching only the sending side was measured to change
 nothing at all: the receiving side then aborts on the same condition, at the
 same threshold, with the same number of retries needed.
 .
 Measured on three Debian nodes synchronising a 31.66 GB tree of 56525 mixed
 files to two peers, with all scheduled jobs stopped:
 .
   unpatched : 7 csync2 runs needed, aborting after 8.0, 15.4 and 22.9 GB
   patched   : 1 csync2 run, no aborts
 .
 The GnuTLS manual requires that gnutls_record_send() be called again "with the
 exact same parameters" after GNUTLS_E_AGAIN or GNUTLS_E_INTERRUPTED, and that
 gnutls_record_recv() simply be called again. The loop below preserves this: the
 offset is only advanced after a successful send, so a retry repeats the
 identical (pointer, length) pair. csync2 uses a blocking socket -- O_NONBLOCK is
 never set on conn_fd_in/conn_fd_out -- so the retry cannot spin: the next call
 blocks in the underlying read()/write().
 .
 The non-TLS branch of WRITE() already loops over partial writes and retries on
 EINTR; this makes the TLS branch behave the same way.
Author: w4zu <[email protected]>
Origin: other
Forwarded: https://github.com/LINBIT/csync2/issues/45
Last-Update: 2026-09-17
--- a/conn.c
+++ b/conn.c
@@ -471,7 +471,13 @@
 {
 #ifdef HAVE_LIBGNUTLS
 	if (csync_conn_usessl)
-		return gnutls_record_recv(conn_tls_session, buf, count);
+	{
+		ssize_t rc;
+		do {
+			rc = gnutls_record_recv(conn_tls_session, buf, count);
+		} while (rc == GNUTLS_E_AGAIN || rc == GNUTLS_E_INTERRUPTED);
+		return (int)rc;
+	}
 	else
 #endif
 		return read(conn_fd_in, buf, count);
@@ -482,8 +488,21 @@
 	static int n, total;
 
 #ifdef HAVE_LIBGNUTLS
-	if (csync_conn_usessl)
-		return gnutls_record_send(conn_tls_session, buf, count);
+	if (csync_conn_usessl) {
+		size_t sent = 0;
+		while (sent < count) {
+			ssize_t rc = gnutls_record_send(conn_tls_session,
+					((const char *)buf) + sent, count - sent);
+			if (rc == GNUTLS_E_AGAIN || rc == GNUTLS_E_INTERRUPTED)
+				continue;
+			if (rc < 0)
+				return sent ? (int)sent : (int)rc;
+			if (rc == 0)
+				break;
+			sent += (size_t)rc;
+		}
+		return (int)sent;
+	}
 	else
 #endif
 	{

Reply via email to