On Monday morning, I had eight emails each notifying me of a failed rsync attempt. This is one of those messages:
From: Cron Daemon <rsyncer@pg02.int.unixathome.org> To: dan@langille.org Subject: Cron <rsyncer@pg02> ~/bin/backup.sh > /dev/null X-Cron-Env: <SHELL=/bin/sh> X-Cron-Env: <MAILTO=dan@langille.org> X-Cron-Env: <PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/usr/X11R6/bin:$HOME/bin; export PATH> X-Cron-Env: <LOGNAME=rsyncer> X-Cron-Env: <USER=rsyncer> Date: Mon, 25 May 2026 02:02:00 +0000 Message-Id: <6a13ad98.43eb8.34b50ce7@pg02.int.unixathome.org> /usr/local/sbin/rrsync error: unsafe arg: / ['', '/usr/home/rsyncer/backups'] rsync: connection unexpectedly closed (0 bytes received so far) [Receiver] rsync error: error in rsync protocol data stream (code 12) at io.c(232) [Receiver=3.4.3]
Well, that’s a new one to me.
This post will eventually show you the solution, and if that’s what you need now, jump to the bottom.
If you’re not in a rush, I’m sure the following reading will be funny and a cure for your insomnia. Wow, … that sounds like Michael W. Lucas writing… Perhaps I should stop writing for free…
In this post:
- FreeBSD 15.0
- rsync-3.4.1
- rsync-3.4.3
- details on the python flavor of the net/rsync FreeBSD port
Here we go.
What I did yesterday
Yesterday, I logged into every host (by that, I mean jail, host, vm, etc) and did sudo pkg install rsync. To be honest, it only the hosts which had rsync-python installed. That package is a flavor of rsync compiled to include python as a dependency.
How did I know which hosts that was?
[21:04 pg01 dvl ~] % psql samdrucker
psql (18.4)
Type "help" for help.
samdrucker=# select * from hostswithpackageshowversion('rsync-python') order by host;
host | package_version
---------------------------------+----------------------
r720-02-pg02.int.unixathome.org | rsync-python-3.4.1_6
zuul.unixathome.org | rsync-python-3.4.1_6
(2 rows)
samdrucker=#
That output is current as of last night, and I need to update those two hosts, but that’s the command I ran. See sysutils/samdruckerserver for more details about this package history tool I wrote.
Why would rsync need python?
Well, let me tell you…
Adding a missing dependency
In April 2025, a FreeBSD PR was filed to add a missing dependency: python. That first commit bit me in that rrsync disappeared. Note that is rrsync, not rsync. rrsync is “a script to setup restricted rsync users via ssh logins”. It means you can restrict an incoming ssh connection to rsync‘ing only specific things. See My solution for copying backups around the homelab.
flavor
Next, a flavor was added, net/rsync@python, named rsync-python. I installed that on all my hosts where rsync was installed.
And things were happy. Until they weren’t.
mariadb
It wasn’t until March 2026 that I filed a new PR – I use rrsync when copying backups to another location. With flavors, I can’t have both net/rsync and net/rsync-python, given that net/rsync is a dependency of databases/mariadb118-server…
And, by the way, the reasons for swapping to maria were frustrating. So far, just that one host has been migrated.
So, what to do…
We initially talked about two distinct ports, neither of which conflicted:
- net/rsync-rrsync
- net/rsync
Eventually, it was decided to go back to what we had before. I was OK with that. No worries.
Reverting from rsync-python to rsync
Yesterday, being a rainy Sunday of USA Memorial Day weekend, I did this on every host which had it:
[11:49 x8dtu dvl ~] % sudo pkg install rsync Updating local repository catalogue... local repository is up to date. All repositories are up to date. Checking integrity... done (1 conflicting) - rsync-3.4.3 [local] conflicts with rsync-python-3.4.1_6 [installed] on /usr/local/bin/rsync Checking integrity... done (0 conflicting) The following 2 package(s) will be affected (of 0 checked): New packages to be INSTALLED: rsync: 3.4.3 [local] Installed packages to be REMOVED: rsync-python: 3.4.1_6 Number of packages to be removed: 1 Number of packages to be installed: 1 Proceed with this action? [y/N]: y [1/2] Deinstalling rsync-python-3.4.1_6... [1/2] Deleting files for rsync-python-3.4.1_6: 100% [2/2] Installing rsync-3.4.3... [2/2] Extracting rsync-3.4.3: 100% ===== Message from rsync-3.4.3: -- Some scripts provided by rsync, such as rrsync, require Python, which is not installed by default.
I went to bed, and woke up to the emails in question.
The solution
I could not figure out the problem at first. I installed the old package (rsync-python-3.4.1_6) – things worked. I installed the new package (rsync-3.4.3) – things broke.
I did a diff between the two rrsync scripts:
[11:49 x8dtu dvl ~] % diff -ruN /usr/local/sbin/rrsync ~/tmp/rrsync
--- /usr/local/sbin/rrsync 2026-05-24 02:02:17.000000000 +0000
+++ /usr/home/dvl/tmp/rrsync 2026-05-25 11:49:17.323864000 +0000
@@ -46,7 +46,6 @@
'compare-dest': 2,
'compress-choice': 1,
'compress-level': 1,
- 'compress-threads': 1,
'copy-dest': 2,
'copy-devices': -1,
'copy-unsafe-links': 0,
@@ -60,7 +59,6 @@
'delete-during': 0,
'delete-excluded': 0,
'delete-missing-args': 0,
- 'dirs': 0,
'existing': 0,
'fake-super': 0,
'files-from': 3,
@@ -302,7 +300,6 @@
if arg.startswith('./'):
arg = arg[1:]
arg = arg.replace('//', '/')
- arg = arg.lstrip('/')
if args.dir != '/':
if HAS_DOT_DOT_RE.search(arg):
die("do not use .. in", opt, "(anchor the path at the root of your restricted dir)")
I was sure that was the cause of the issue.
Searching for “/usr/local/sbin/rrsync error: unsafe arg: /” gave me:
This error is almost entirely caused by rrsync (restricted rsync), which is a wrapper script on the remote server meant to restrict users to specific directories and prevent them from escaping to the root / directory.
EH? I’m not doing that. (disclosure: oh yes I was!)
I could not see the cause of the problem, so I composed my social media post. In the process of that writing, I discovered the cause. It was a /.
It was the / in this file (and others like it). See the last line of the file.
[rsyncer@dbclone ~/backups/x8dtu-pg01/database-backup/postgresql]$ cat /home/rsyncer/bin/rsync-backup-from-x8dtu-pg01.sh
#!/bin/sh
#
# This file does a backup of each database on the server.
# It relies upon a file on the server to do the actual backup,
# then uses rsync to copy the files from the server to here.
#
# the ~/.ssh/authorized keys entry on the server must look like this:
#
# from="10.55.0.140",command="/usr/local/sbin/rrsync /usr/home/backups/" [ssh key goes here]
#
# invoking rrsync ensures the incoming rsync process is chrooted to /usr/home/dan/backups/
# Thus, BACKUPDIR is relative to that location.
BACKUPDIR=${HOME}/backups/x8dtu-pg01
IDENTITY_FILE_RSYNC=${HOME}/.ssh/id_ed25519.rsync.pg01.from.x8dtu
SERVER_TO_RSYNC=x8dtu.vpn.unixathome.org
cd ${BACKUPDIR}
#
# use rsync to get local copies
#
/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
What / you might ask? The one right after the hostname: ${SERVER_TO_RSYNC}:/
I removed that, everything worked.
Fixing more stuff
It seems I have a few of those:
[rsyncer@dbclone ~/bin]$ grep ':/ ' *
rsync-backup-from-aws-1-dump.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-gelt.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' --exclude Bacula ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-mysql01.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-mysql02.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-papers.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-pg01.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-pg02.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-pg03.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-supernews.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-tallboy.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-x8dtu-pg01.sh~:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-x8dtu-pg02.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-x8dtu.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-zuul-mariadb01.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-zuul-mysql.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-zuul-pg01.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync-backup-from-zuul-pg02.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync.pg01.from.r720-02.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
rsync.pg02.from.r720-02.sh:/usr/local/bin/rsync -e "/usr/bin/ssh -i ${IDENTITY_FILE_RSYNC}" --recursive -av --stats --progress --exclude 'archive' ${SERVER_TO_RSYNC}:/ ${BACKUPDIR}
Time to update them all:
[rsyncer@dbclone ~/bin]$ joe $(grep -l ':/ ' *)
That finds each file with the string in question, and opens it into an editor for me. I could have done a global edit, changing every occurrence with review. I feel that’s too open to error.
Changed them all. Now I wait. I’ll post this entry tomorrow, after seeing how things go.
Tuesday morning
It is now Tuesday morning at 11:06 UTC – no errors. No monitoring flags. All good to go.











