hlink: fix hard-link failure abort (#741) and the latent drain hang it exposes

When a hard-link group member fails its direct link() (EMLINK or
EXDEV) it is written as a regular file and finishes first, claiming
the group's prior_hlinks node; a later member's finish then hits the
"Hlink node data ... already has path" check and aborts the whole
transfer (code 13).  Tolerate the duplicate finish: keep the first
leader's path, log an FINFO warning, and continue.

Removing the abort exposes a second, latent bug: finish_hard_link()
decrements flist->in_progress for every file in the completion chain,
but members that failed their direct link() in hard_link_check() were
never counted, so the counter drifts negative and the final drain
hangs forever.  Add FLAG_HLINK_WAIT, set at the three increment
sites, and gate the two affected decrements on it.

Tested with a 65000-link pinned-inode EMLINK fixture (full repro and
evidence in the PR description), an -H --link-dest regression matrix,
and the upstream hardlink/link-dest test suite.
hlink: fix hard-link failure abort (#741) and the latent drain hang it exposes

When a hard-link group member fails its direct link() (EMLINK or
EXDEV) it is written as a regular file and finishes first, claiming
the group's prior_hlinks node; a later member's finish then hits the
"Hlink node data ... already has path" check and aborts the whole
transfer (code 13).  Tolerate the duplicate finish: keep the first
leader's path, log an FINFO warning, and continue.

Removing the abort exposes a second, latent bug: finish_hard_link()
decrements flist->in_progress for every file in the completion chain,
but members that failed their direct link() in hard_link_check() were
never counted, so the counter drifts negative and the final drain
hangs forever.  Add FLAG_HLINK_WAIT, set at the three increment
sites, and gate the two affected decrements on it.

Tested with a 65000-link pinned-inode EMLINK fixture (full repro and
evidence in the PR description), an -H --link-dest regression matrix,
and the upstream hardlink/link-dest test suite.
This commit is contained in:
John Bowman committed 2026-10-31 12:24:00 -07:00
1 parent 8718cd28c5
commit 350425a81d
3 files changed
+21 -4

No files matched your search

+6 -1
View File
@@ -2253,6 +2253,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
if (statret != 0) {
#ifdef SUPPORT_HARD_LINKS
if (preserve_hard_links && F_HLINK_NOT_LAST(file)) {
file->flags |= FLAG_HLINK_WAIT;
cur_flist->in_progress++;
goto cleanup;
}
@@ -2358,6 +2359,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
/* pretend the file didn't exist */
#ifdef SUPPORT_HARD_LINKS
if (preserve_hard_links && F_HLINK_NOT_LAST(file)) {
file->flags |= FLAG_HLINK_WAIT;
cur_flist->in_progress++;
goto cleanup;
}
@@ -2595,7 +2597,10 @@ static void handle_skipped_hlink(struct file_struct *file, int itemizing,
return;
file = cur_flist->files[new_last_ndx - cur_flist->ndx_start];
cur_flist->in_progress--; /* undo prior increment */
if (file->flags & FLAG_HLINK_WAIT) {
file->flags &= ~FLAG_HLINK_WAIT;
cur_flist->in_progress--; /* undo prior increment */
}
f_name(file, fbuf);
recv_generator(fbuf, file, new_last_ndx, itemizing, code, f_out);
+14 -3
View File
@@ -335,6 +335,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
F_HL_PREV(file) = F_HL_PREV(prev_file);
F_HL_PREV(prev_file) = ndx;
file->flags |= FLAG_FILE_SENT;
file->flags |= FLAG_HLINK_WAIT;
cur_flist->in_progress++;
if (DEBUG_GTE(HLINK, 2)) {
rprintf(FINFO, "hlink for %d (%s,%d): waiting for %d\n",
@@ -535,7 +536,10 @@ void finish_hard_link(struct file_struct *file, const char *fname, int fin_ndx,
prev_statret = link_stat(f_name(file, prev_name), &prev_sx.st, 0);
val = maybe_hard_link(file, ndx, prev_name, prev_statret, &prev_sx,
our_name, stp, fname, itemizing, code);
flist->in_progress--;
if (file->flags & FLAG_HLINK_WAIT) {
file->flags &= ~FLAG_HLINK_WAIT;
flist->in_progress--;
}
free_stat_x(&prev_sx);
if (val < 0)
continue;
@@ -555,9 +559,16 @@ void finish_hard_link(struct file_struct *file, const char *fname, int fin_ndx,
exit_cleanup(RERR_MESSAGEIO);
}
if (CVAL(node->data, 0) != 0) {
rprintf(FERROR, "Hlink node data for %d already has path=%s (%s)\n",
/* A finished leader was already recorded for this group.
* This can happen when an earlier member of the group failed
* to hard-link (e.g. EMLINK or EXDEV) and was written as a
* regular file, then finished. Keep the original leader's
* path and tolerate the duplicate finish instead of aborting
* the whole transfer. (Upstream: RsyncProject/rsync#741.) */
rprintf(FINFO,
"hlink: group %d already finished at path=%s (%s); ignoring duplicate finish\n",
gnum, (char*)node->data, f_name(file, prev_name));
exit_cleanup(RERR_MESSAGEIO);
return;
}
free(node->data);
node->data = strdup(our_name);
+1
View File
@@ -93,6 +93,7 @@
#define FLAG_TIME_FAILED (1<<11)/* generator */
#define FLAG_MOD_NSEC (1<<12) /* sender/receiver/generator */
#define FLAG_GOT_DIR_FLIST (1<<13)/* sender/receiver/generator - dir_flist only */
#define FLAG_HLINK_WAIT (1<<14) /* generator (w/FLAG_HLINKED): counted in in_progress, pending hard-link processing */
/* These flags are passed to functions but not stored. */