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
{