Repository: borgbackup/borg

Issue:
can't use borg mount-ed device file backup for loop device
From IRC:

It does not seem I can mount a device via a loop device (too bad, wanted to mount it without having to extract it):

```
# borg mount -o allow_root,dev,suid /repo::today /mnt
# losetup -f -r /mnt/dev/dm-18 
losetup: /mnt/dev/dm-18: failed to set up loop device: No such device or address
```



Discussion:
```
(borg-env)tw@tux:~/w/borg$ borg --version
borg 1.0.4.dev62+ng02c72b9
(borg-env)tw@tux:~/w/borg$ borg create --read-special repo::spe /dev/sda1
(borg-env)tw@tux:~/w/borg$ borg list repo::spe -v
brw-rw-r-- root   disk          0 Fri, 2016-07-01 13:25:41 dev/sda1
(borg-env)tw@tux:~/w/borg$ borg mount repo::spe mnt
(borg-env)tw@tux:~/w/borg$ ls -l mnt/dev/sda1
brw-rw-r-- 1 root disk 0, 0 Jul  1 13:25 mnt/dev/sda1
(borg-env)tw@tux:~/w/borg$ sudo mount -o loop,ro mnt/dev/sda1 mnt2
mount: mnt/dev/sda1: failed to setup loop device: Permission denied
```

Hmm, I suspect it is confused by `mnt/dev/sda1` mode saying it is a block device, but it should be "regular file".

Considering that, I guess that this was not a sufficient fix:
https://github.com/borgbackup/borg/pull/1216/files

Guess if we open and read the special file (like block device) like a regular file, we also have to reflect "regular" file in the item's metadata in the archive (and can not store the original mode there).

Can you try `sudo losetup -P /dev/loop0 mnt/dev/sda1` and then `fdisk -l /dev/loop0` and `ls /dev/loop0*`? (assuming you don't have any loop devices mounted, if you do use loop1 or similar)

@PlasmaPower what should running fdisk on a partition without MBR be good for?


Resolving patch:
diff --git a/borg/archive.py b/borg/archive.py
--- a/borg/archive.py
+++ b/borg/archive.py
@@ -46,6 +46,11 @@
 flags_noatime = flags_normal | getattr(os, 'O_NOATIME', 0)
 
 
+def is_special(mode):
+    # file types that get special treatment in --read-special mode
+    return stat.S_ISBLK(mode) or stat.S_ISCHR(mode) or stat.S_ISFIFO(mode)
+
+
 class BackupOSError(Exception):
     """
     Wrapper for OSError raised while accessing backup files.
@@ -589,9 +594,16 @@ def process_file(self, path, st, cache, ignore_inode=False):
                 return status
             else:
                 self.hard_links[st.st_ino, st.st_dev] = safe_path
-        path_hash = self.key.id_hash(os.path.join(self.cwd, path).encode('utf-8', 'surrogateescape'))
+        is_special_file = is_special(st.st_mode)
+        if not is_special_file:
+            path_hash = self.key.id_hash(os.path.join(self.cwd, path).encode('utf-8', 'surrogateescape'))
+            ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode)
+        else:
+            # in --read-special mode, we may be called for special files.
+            # there should be no information in the cache about special files processed in
+            # read-special mode, but we better play safe as this was wrong in the past:
+            path_hash = ids = None
         first_run = not cache.files
-        ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode)
         if first_run:
             logger.debug('Processing files ...')
         chunks = None
@@ -616,10 +628,17 @@ def process_file(self, path, st, cache, ignore_inode=False):
                     chunks.append(cache.add_chunk(self.key.id_hash(chunk), chunk, self.stats))
                     if self.show_progress:
                         self.stats.show_progress(item=item, dt=0.2)
-            cache.memorize_file(path_hash, st, [c[0] for c in chunks])
+            if not is_special_file:
+                # we must not memorize special files, because the contents of e.g. a
+                # block or char device will change without its mtime/size/inode changing.
+                cache.memorize_file(path_hash, st, [c[0] for c in chunks])
             status = status or 'M'  # regular file, modified (if not 'A' already)
         item[b'chunks'] = chunks
         item.update(self.stat_attrs(st, path))
+        if is_special_file:
+            # we processed a special file like a regular file. reflect that in mode,
+            # so it can be extracted / accessed in FUSE mount like a regular file:
+            item[b'mode'] = stat.S_IFREG | stat.S_IMODE(item[b'mode'])
         self.stats.nfiles += 1
         self.add_item(item)
         return status
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -29,7 +29,7 @@
 from .repository import Repository
 from .cache import Cache
 from .key import key_creator, RepoKey, PassphraseKey
-from .archive import backup_io, BackupOSError, Archive, ArchiveChecker, CHUNKER_PARAMS
+from .archive import backup_io, BackupOSError, Archive, ArchiveChecker, CHUNKER_PARAMS, is_special
 from .remote import RepositoryServer, RemoteRepository, cache_if_remote
 
 has_lchflags = hasattr(os, 'lchflags')
@@ -256,15 +256,7 @@ def _process(self, archive, cache, matcher, exclude_caches, exclude_if_present,
             return
 
         try:
-            # usually, do not follow symlinks (if we have a symlink, we want to
-            # backup it as such).
-            # but if we are in --read-special mode, we later process <path> as
-            # a regular file (we open and read the symlink target file's content).
-            # thus, in read_special mode, we also want to stat the symlink target
-            # file, for consistency. if we did not, we also have issues extracting
-            # this file, as it would be in the archive as a symlink, not as the
-            # target's file type (which could be e.g. a block device).
-            st = os.stat(path, follow_symlinks=read_special)
+            st = os.lstat(path)
         except OSError as e:
             self.print_warning('%s: %s', path, e)
             return
@@ -277,7 +269,7 @@ def _process(self, archive, cache, matcher, exclude_caches, exclude_if_present,
         # Ignore if nodump flag is set
         if has_lchflags and (st.st_flags & stat.UF_NODUMP):
             return
-        if stat.S_ISREG(st.st_mode) or read_special and not stat.S_ISDIR(st.st_mode):
+        if stat.S_ISREG(st.st_mode):
             if not dry_run:
                 try:
                     status = archive.process_file(path, st, cache, self.ignore_inode)
@@ -309,13 +301,26 @@ def _process(self, archive, cache, matcher, exclude_caches, exclude_if_present,
                                   read_special=read_special, dry_run=dry_run)
         elif stat.S_ISLNK(st.st_mode):
             if not dry_run:
-                status = archive.process_symlink(path, st)
+                if not read_special:
+                    status = archive.process_symlink(path, st)
+                else:
+                    st_target = os.stat(path)
+                    if is_special(st_target.st_mode):
+                        status = archive.process_file(path, st_target, cache)
+                    else:
+                        status = archive.process_symlink(path, st)
         elif stat.S_ISFIFO(st.st_mode):
             if not dry_run:
-                status = archive.process_fifo(path, st)
+                if not read_special:
+                    status = archive.process_fifo(path, st)
+                else:
+                    status = archive.process_file(path, st, cache)
         elif stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
             if not dry_run:
-                status = archive.process_dev(path, st)
+                if not read_special:
+                    status = archive.process_dev(path, st)
+                else:
+                    status = archive.process_file(path, st, cache)
         elif stat.S_ISSOCK(st.st_mode):
             # Ignore unix sockets
             return
@@ -1134,7 +1139,8 @@ def build_parser(self, args=None, prog=None):
                                     'lzma,0 .. lzma,9 == lzma (with level 0..9).')
         subparser.add_argument('--read-special', dest='read_special',
                                action='store_true', default=False,
-                               help='open and read special files as if they were regular files')
+                               help='open and read block and char device files as well as FIFOs as if they were '
+                                    'regular files. Also follows symlinks pointing to these kinds of files.')
         subparser.add_argument('-n', '--dry-run', dest='dry_run',
                                action='store_true', default=False,
                                help='do not create a backup archive')


Test patch: