Repository: borgbackup/borg

Issue:
`borg create -x` traverses filesystems if parent_fs == child_fs
I'm currently migrating towards borgbackup. As always while switching infrastructure, you sometimes hit old yet unknown bugs: For some reasons my root-partition was mounted on `/` and for some reasons on `/mnt/`, too.

While creating a borgbackup archive of `/`, borg traverses the mountpoint of `/mnt`. It's the same filesystem (but it's still a mountpoint!).

Although calling `borg create ` with `-x`-option, both `/` and `/mnt` get archived.

```
[root@nsd:~]$ mount | grep /dev/vda
/dev/vda2 on / type ext4 (rw,relatime,data=ordered)
/dev/vda2 on /mnt type ext4 (rw,relatime,data=ordered)
[root@nsd:~]$ borg create -x steak:nsd::$(date +'%Y-%m-%d-%H:%M') /
[root@nsd ~]$ borg info steak:nsd::2017-02-11-15:27
Name: 2017-02-11-15:27
Fingerprint: c4775b04d5ebf8d81b5243b59616ba191290b5b9663d8392a558250f05d086b2
Hostname: nsd
Username: root
Time (start): Sat, 2017-02-11 15:27:58
Time (end):   Sat, 2017-02-11 15:34:33
Command line: /usr/bin/borg create -x steak:nsd::2017-02-11-15:27 /
Number of files: 132670

                       Original size      Compressed size    Deduplicated size
This archive:                4.85 GB              4.85 GB              2.41 GB
All archives:                4.85 GB              4.85 GB              2.41 GB

                       Unique chunks         Total chunks
Chunk index:                   61225               133578
[root@nsd ~]$ borg list --short steak:nsd::2017-02-11-15:27  | wc -l
160025
[root@nsd ~]$ borg list --short steak:nsd::2017-02-11-15:27 | grep '^mnt' | wc -l
80015
```
So, creating an archive always yields to the double the Original Size but because of borg's deduplication strategy this is just a minor issue.

Also someone has to be as stupid as me and has to mount the backed up volume twice.

But pain starts when extracting the archive, when copying out the doubled size of the harddisk and restoring takes doubled time.


Discussion:
I'm not sure if there even is a ~portable way to check for this specific case. I would expect that eg. all the stuff from statvfs() would be the same (since it's the same physical FS) and that the device from stat() would also be the same (same physical device as well). I suspect the only way, if one wanted to fix this, would be to actually enumerate mounted filesystems, which is not portable afaik.

It's a funny situation, where "stay in same file system, do not cross mount points" is both true and false: it stays in the same file system, but did cross over a mount point.
Yes. And if we try to enumerate mountpoints (which is not easy in the general case with namespaces and filesystems hiding each other) we should check not to make the same mistakes obnam made so it had to roll back the changes (iirc).

bebehei noted that `find` and `mountpoint` both detect something. In case of `find` is seems to be a general cycle detection (storing all dev/inode tuples and bailing out if one repeats) and `mountpoint` has this very strange special caseing:

```
	r = (st.st_dev != st2.st_dev) ||
	    (st.st_dev == st2.st_dev && st.st_ino == st2.st_ino);
```
This would make sense if `st` where the assumed root directory of an FS and `st2` is the suspected mountpoint. If we see the (dev, ino) of the current FS root directory again, then that would mean that it's a cycle.

So it might work along these lines:

- Start at path /foo/bar/bus
- Travel upwards till / or st_dev changes
- Note st_dev, st_ino (if changed: one level before the change) -> this is the presumed root directory of the path we're working on (or not, --bind, btrfs, ZFS)
- When inspecting a directory and -x is in effect
  - Check if sts_dev (st suspect) != st_dev => don't recurse
  - Check if sts_dev == st_dev && sts_ino == st_ino => don't recurse

I'd expect that you can only catch --bind by doing what find does, which we don't want to do.
May a simple way would be to remember all dev/inode pairs of all parent directories and see is anything repeats. Not sure it is worth it though. Because bind mounts still are likely undetected. And while it is a bit more general than what `mountpoint` does is seems still like quite a special case.
Yes. Perhaps just adjust the wording of the --one-file-system option so that it "legally" includes this possibility. Something like "_stay in the same file system, do not traverse into other file systems and do not store mount points_". (The latter clarification is something I wanted to put there since #1033)
> bebehei noted that find and mountpoint both detect something.

Researching again: find's algorithm is wrong.

```
[root@buildbot:/] # mount /dev/vda2 /home/asdf
[root@buildbot:/] # find /home -mount | grep /root file
find: File system loop detected; '/home/asdf/home' is part of the same file system loop as '/home'.
[...]
/home/asdf/root/.ssh
[...]
```

find only won't traverse into the same subdirectory of this filesystem, but it will include the rest of the FS.

Today I got heavily disappointed of the `-x`, `-mount` and `--one-filesystem` options in common Unix programs.

> Yes. Perhaps just adjust the wording of the --one-file-system option so that it "legally" includes this possibility.

@enkore Yes, this is the best way to "fix" it.
"It's not their fault" ™ -- the semantics that you can mount the same FS twice, or have bind mounts, or mount subvolumes (btrfs, ZFS) are all quite new and were not present when most of these were written (at least I think so)... or the portable-ish APIs meant to deal with it (stat, statvfs) :)

Resolving patch:
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -1373,7 +1373,7 @@ def build_parser(self, args=None, prog=None):
                                help='write checkpoint every SECONDS seconds (Default: 300)')
         subparser.add_argument('-x', '--one-file-system', dest='one_file_system',
                                action='store_true', default=False,
-                               help='stay in same file system, do not cross mount points')
+                               help='stay in same file system')
         subparser.add_argument('--numeric-owner', dest='numeric_owner',
                                action='store_true', default=False,
                                help='only store numeric user and group identifiers')


Test patch: