Ok, mystery solved - this fix will help anybody who is backing up via ssh to directories that are symlinks on the remote machine.
My destination server is running slackware, with the backup ssh account using bash as the default shell, but things should be pretty similar with other distros. The notes below assume that USER is the user account on the destination machine.
I back up to directories of the form /home/USER/backup/%d-%m-%Y
) where /home/USER/backup
is a symlink to another directory (one on an external USB disk, as it happens.)
The script /usr/libexec/webmin/virtual-server/backups-lib.pl
uses the command “ls -l” to list the directory contents:
local $lscmd = $sshcmd." LANG=C ls -l ".quotemeta($base);
local $lsout = &run_ssh_command($lscmd, $pass, $err, $asuser);
(see lines 4394-4395). It does not add a trailing slash to the directory name, and there is no way to store the directory name with a trailing slash in the backup configuration in virtualmin (that was one of the first things I’d tried, and any trailing slash is stripped off.)
The backup script uses the ls -l
command to get a listing of all the files in the directory. Unfortunately when you do “ls -l” on a symlinked directory with no trailing slash on the directory name, you don’t get a listing of the content, but rather the detail of the symlink:
ls -l /home/USER/backup
lrwxrwxrwx 1 root root 36 Apr 29 16:46 /home/USER/backup -> /mnt/vpsbackup/
This explains why the backup script was not finding any candidate backups for deletion.
In an interactive shell, you can add the following to .bashrc so that symlinks are properly expanded and directory contents listed instead of symlink details:
alias ls=‘ls --dereference-command-line-symlink-to-dir’
The command “ls -l” will then follow the symlink and produce the expected list of the contents of the symlinked directory, even if you don’t add the trailing slash.
However, by default, your .bashrc won’t be loaded when a command is run non-interactively via ssh. To fix this on my distro I had to do three things:
1 - Allow user environments to be accessed for ssh commands. Make sure the following is set in /etc/ssh/sshd_config
(or wherever your sshd_config is kept):
PermitUserEnvironment yes
(Note: apparently some distros may have this option disabled in the source, so you will need to recompile your own ssh packages if so.)
2 - Add the following line to USER’s .bashrc BEFORE the alias line, to enable alias expansion in ssh non-interactive shells:
shopt -s expand_aliases
3 - Create a file called /home/USER/.ssh/environment that contains the following line to configure the environment during ssh access:
BASH_ENV=/home/USER/.bashrc
Once all that’s in place, old backups are successfully cleaned up.
Or you could just hack backups-lib.pl to add a trailing slash, of course! 
As a development request, it would be great if the backup script could explicitly check for and correctly manage this situation for ssh backups, by adding the trailing slash where required (e.g. as an extra check if “ls -l” didn’t produce the expected list of files.)