Repository: borgbackup/borg

Issue:
borg-check doesn't handle one case of truncated metadata chunks
https://gist.github.com/esteigler/7a9b21a7b91d20b6463bdc30e9bcbd80

``` pytraceback
Exception while unpacking item metadata [chunk: 020920_c393225222c0b08962d01582c2ae30adb488becda8b23fde3cd651a02f89037f]
Local Exception.
Traceback (most recent call last):
  File "borg/archiver.py", line 1609, in main
  File "borg/archiver.py", line 1546, in run
  File "borg/archiver.py", line 81, in wrapper
  File "borg/archiver.py", line 148, in do_check
  File "borg/archive.py", line 823, in check
  File "borg/archive.py", line 1058, in rebuild_refcounts
  File "borg/archive.py", line 1015, in robust_iterator
  File "borg/archive.py", line 801, in __next__
  File "msgpack/_unpacker.pyx", line 469, in msgpack._unpacker.Unpacker.__next__ (msgpack/_unpacker.cpp:469)
  File "msgpack/_unpacker.pyx", line 400, in msgpack._unpacker.Unpacker._unpack (msgpack/_unpacker.cpp:400)
TypeError: unhashable type: 'StableDict'
```

What likely happened here is that one metadata chunk was truncated in a msgpack-map (#1138) where a key is expected. As the next chunk contains an item dict which is not hashable and therefore not a valid key in any dict. This error condition is not handled by RobustUnpacker, but should be.

A possible workaround is to debug-delete-obj the metadata chunks (one should be enough) causing the error, which switches to resync-mode in the RobustUnpacker.

---

:moneybag: [there is a bounty for this](https://www.bountysource.com/issues/36171983-borg-check-doesn-t-handle-one-case-of-truncated-metadata-chunks)



Discussion:

Resolving patch:
diff --git a/borg/archive.py b/borg/archive.py
--- a/borg/archive.py
+++ b/borg/archive.py
@@ -754,6 +754,9 @@ def valid_msgpacked_dict(d, keys_serialized):
 class RobustUnpacker:
     """A restartable/robust version of the streaming msgpack unpacker
     """
+    class UnpackerCrashed(Exception):
+        """raise if unpacker crashed"""
+
     def __init__(self, validator, item_keys):
         super().__init__()
         self.item_keys = [msgpack.packb(name) for name in item_keys]
@@ -776,6 +779,14 @@ def __iter__(self):
         return self
 
     def __next__(self):
+        def unpack_next():
+            try:
+                return next(self._unpacker)
+            except (TypeError, ValueError) as err:
+                # transform exceptions that might be raised when feeding
+                # msgpack with invalid data to a more specific exception
+                raise self.UnpackerCrashed(str(err))
+
         if self._resync:
             data = b''.join(self._buffered_data)
             while self._resync:
@@ -788,17 +799,17 @@ def __next__(self):
                 self._unpacker = msgpack.Unpacker(object_hook=StableDict)
                 self._unpacker.feed(data)
                 try:
-                    item = next(self._unpacker)
+                    item = unpack_next()
+                except (self.UnpackerCrashed, StopIteration):
+                    # as long as we are resyncing, we also ignore StopIteration
+                    pass
+                else:
                     if self.validator(item):
                         self._resync = False
                         return item
-                # Ignore exceptions that might be raised when feeding
-                # msgpack with invalid data
-                except (TypeError, ValueError, StopIteration):
-                    pass
                 data = data[1:]
         else:
-            return next(self._unpacker)
+            return unpack_next()
 
 
 class ArchiveChecker:
@@ -1017,6 +1028,9 @@ def valid_item(obj):
                                 yield item
                             else:
                                 report('Did not get expected metadata dict when unpacking item metadata', chunk_id, i)
+                    except RobustUnpacker.UnpackerCrashed as err:
+                        report('Unpacker crashed while unpacking item metadata, trying to resync...', chunk_id, i)
+                        unpacker.resync()
                     except Exception:
                         report('Exception while unpacking item metadata', chunk_id, i)
                         raise


Test patch: