Bug 13901 - Empty quotes adds cwd to SRC directories
Summary: Empty quotes adds cwd to SRC directories
Status: RESOLVED FIXED
Alias: None
Product: rsync
Classification: Unclassified
Component: core (show other bugs)
Version: 3.1.3
Hardware: x64 Linux
: P5 normal (vote)
Target Milestone: ---
Assignee: Wayne Davison
QA Contact: Rsync QA Contact
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2019-04-17 13:54 UTC by Daniel Alm Grundström
Modified: 2020-07-27 21:55 UTC (History)
0 users

See Also:


Attachments
For cmd 'rsync "$UNSET_VAR" --debug=ALL5 --verbose --recursive -- /data/src/ /data/dest/' (4.51 KB, text/x-log)
2019-04-17 13:54 UTC, Daniel Alm Grundström
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Daniel Alm Grundström 2019-04-17 13:54:54 UTC
Created attachment 15076 [details]
For cmd 'rsync "$UNSET_VAR" --debug=ALL5 --verbose --recursive -- /data/src/ /data/dest/'

Hi! It's my first bug report here so let me know if I should clarify anything!

If you add empty quotes to the rsync command line, it is interpreted as the current working directory ("."), and added to the SRC args. It doesn't matter if the quotes come before or after any options, if it comes before "--" or if there are other source directories specified.

This is a problem if you specify quoted bash variables on the command line and one of them happens to be unset.

/data/cwd $ ls
file-i-dont-want-to-copy.txt
/data/cwd $ rsync "$UNSET_VAR" --recursive --verbose -- /data/src/ /data/dest/
sending incremental file list
file-i-dont-want-to-copy.txt
file-i-want-to-copy.jpg

sent 819,001 bytes  received 54 bytes  1,638,110.00 bytes/sec
total size is 818,581  speedup is 1.00

I have attached the output of the command with '--debug=ALL5' added, if it helps.
Comment 1 Dave Gordon 2019-04-19 18:36:22 UTC
It probably wouldn't be difficult to spot the case you've identified and change the behaviour; I suspect you'd just need to change this code around line 2134 of flist.c, in send_file_list() to handle the case of (!len) separately (e.g. error, or ignore):

                len = strlen(fbuf);
                if (relative_paths) {
                        /* We clean up fbuf below. */
                        name_type = NORMAL_NAME;
>>>>            } else if (!len || fbuf[len - 1] == '/') {
                        if (len == 2 && fbuf[0] == '.') {
                                /* Turn "./" into just "." rather than "./." */
                                fbuf[--len] = '\0';
                        } else {
                                if (len + 1 >= MAXPATHLEN)
                                        overflow_exit("send_file_list");
                                fbuf[len++] = '.';
                                fbuf[len] = '\0';
                        }
                        name_type = DOTDIR_NAME;
                } else if (len > 1 && fbuf[len-1] == '.' && fbuf[len-2] == '.'

BUT there are other parts of the code that have already processed the arglist and in some cases rewritten it, and argpath parsing is already pretty complicated, because of handling lots of different cases (local or remote source(s), local or remote target, different syntaxes (rsync://host[/path], [host:]path, host::module[/path]), etc).

In particular, an empty pathname part when accessing a daemon-defined module will already have been rewritten as ".", and since this is a useful (and probably widely-used) case, you wouldn't want to change that. In which case, you would have to accept an inconsistency between daemon (module) access, where empty does (and should) mean "." and non-daemon mode, where you want it rejected.

As a simple workaround, consider using the shell syntax "${VAR?}" which will cause a shell error exit if the variable is unset -- which will make the problem obvious and reasonably easy to debug.

$ rsync -aSHAXuvn "${VAR?}" ~/bin/ /tmp/
bash: VAR: parameter null or not set

Or ${VAR:+"$VAR"}"${VAR:-mydefault}" which will insert VAR if set, or the default of your choice in the event that VAR is somehow unset. Depending on what outcome you want, you could set it to a flag that rsync doesn't recognise, and then rsync will complain about the command line:

$ rsync -aSHAXuvn ${VAR:+"$VAR"}"${VAR:---VAR-unset}" ~/bin/ /tmp/
rsync: --VAR-unset: unknown option
rsync error: syntax or usage error (code 1) at main.c(1572) [client=3.1.1]

If the chosen default is a path that doesn't exist you'll get a nonfatal error from rsync, but any other files you intended to copy will still get copied.

rsync -aSHAXuvn ${VAR:+"$VAR"}"${VAR:-./nosuchfile}" ~/bin/ /tmp/
sending incremental file list
rsync: link_stat "./nosuchfile" failed: No such file or directory (2)
./
[...]

sent 2,587 bytes  received 873 bytes  6,920.00 bytes/sec
total size is 791,762  speedup is 228.83 (DRY RUN)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1183) [sender=3.1.1]

Hope this helps,
Dave
Comment 2 Wayne Davison 2020-07-27 21:55:32 UTC
While I also agree that you want to build in unset-var safety into your scripts (I a zsh option set that makes all unset vars fatal, for instance), I do also agree that specifying a totally empty arg to rsync is not useful, so I made rsync complain about it.  This doesn't change an arg such as "host:" implying the home directory, so it won't help to safeguard a "$HOST:$DIR" expansion.