Skip to content

Commit 15738b8

Browse files
Merge pull request #10517 from ThomasWaldmann/robustunpacker-pypy-bufferror
RobustUnpacker: release all memoryviews of the resync buffer before resizing it
2 parents 0ff5aa9 + 4da7021 commit 15738b8

3 files changed

Lines changed: 34 additions & 11 deletions

File tree

‎docs/changes.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ Fixes:
175175
- legacy remote: use deques for the RPC call queues (performance fix for
176176
transfer), #10450
177177
- check --repair: RobustUnpacker: resync in linear time (performance fix), #10459
178+
- check --repair: RobustUnpacker: release all memoryviews of the resync buffer before
179+
resizing it (BufferError on PyPy nightly)
178180
- import-tar: convert PAX ACLs to borg's ACL format
179181
- validate BORG_PACK_MAX_SIZE and BORG_PACK_MAX_COUNT, #10485
180182
- completion: repository directory completion fixes, #10460

‎src/borg/archive.py‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2205,21 +2205,25 @@ def process_file(self, *, tarinfo, status, type, tar):
22052205
INCOMPLETE = object()
22062206

22072207

2208-
def valid_msgpacked_dict(d):
2209-
"""check if the data <d> looks like a msgpacked item dict
2208+
def valid_msgpacked_dict(d, offset=0):
2209+
"""check if the data <d> at <offset> looks like a msgpacked item dict
22102210
22112211
That is: a msgpack map whose first key is a msgpack str that looks like an item key. Items are
22122212
packed with sorted keys, so the first key is the alphabetically smallest key of the item. The key
22132213
is not checked against a list of known keys, so items written by a newer borg version (with keys
22142214
this version does not know) are found as well.
2215+
2216+
<d> is only indexed, never sliced: so <d> can be a memoryview and no child views of it are created
2217+
(on PyPy, a child view that was not released keeps the viewed object exported, i.e. not resizable,
2218+
until the garbage collector collects the view).
22152219
"""
22162220
d_len = len(d)
2217-
if d_len == 0:
2221+
if d_len <= offset:
22182222
return False
2219-
if d[0] & 0xF0 == 0x80: # object is a fixmap (up to 15 elements)
2220-
offs = 1
2221-
elif d[0] == 0xDE: # object is a map16 (up to 2^16-1 elements)
2222-
offs = 3
2223+
if d[offset] & 0xF0 == 0x80: # object is a fixmap (up to 15 elements)
2224+
offs = offset + 1
2225+
elif d[offset] == 0xDE: # object is a map16 (up to 2^16-1 elements)
2226+
offs = offset + 3
22232227
else:
22242228
# object is not a map (dict)
22252229
# note: we must not have dicts with > 2^16-1 elements
@@ -2237,11 +2241,11 @@ def valid_msgpacked_dict(d):
22372241
else:
22382242
# key is not a (short) str
22392243
return False
2240-
key = d[key_offs : key_offs + key_len]
2241-
if key_len == 0 or len(key) < key_len:
2244+
key_end = key_offs + key_len
2245+
if key_len == 0 or d_len < key_end:
22422246
return False
22432247
# does the key look like an item key?
2244-
return all(c in ITEM_KEY_CHARS for c in key)
2248+
return all(d[i] in ITEM_KEY_CHARS for i in range(key_offs, key_end))
22452249

22462250

22472251
class RobustUnpacker:
@@ -2301,6 +2305,10 @@ def _search(self):
23012305
searched_end = max(self._search_offset, data_len - MAX_ITEM_HEADER_LEN)
23022306
pending_offsets = [o for o in self._pending_offsets if data_len - o <= self.MAX_PENDING_ITEM_LEN]
23032307
self._pending_offsets = []
2308+
# _buffered_data is resized below (and by feed()), which fails while a memoryview of it exists. CPython
2309+
# frees an unreferenced view at once, PyPy only when the garbage collector runs, so every view of
2310+
# _buffered_data is released explicitly: data here, the slice in _try_item_start. valid_msgpacked_dict
2311+
# gets (data, offset) instead of a slice of data, it only indexes data.
23042312
with memoryview(self._buffered_data) as data:
23052313
for offset in pending_offsets:
23062314
item = self._try_item_start(data, offset)
@@ -2309,7 +2317,7 @@ def _search(self):
23092317
elif item is not None:
23102318
return item
23112319
for offset in range(self._search_offset, data_len):
2312-
if not valid_msgpacked_dict(data[offset:]):
2320+
if not valid_msgpacked_dict(data, offset):
23132321
continue
23142322
item = self._try_item_start(data, offset)
23152323
if item is INCOMPLETE:

‎src/borg/testsuite/archive_test.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,19 @@ def test_key_length_msgpacked_items():
516516
assert valid_msgpacked_dict(msgpack.packb(data))
517517

518518

519+
def test_valid_msgpacked_dict_offset():
520+
packed = msgpack.packb({"path": b"/a/b/c"})
521+
data = b"garbage" + packed
522+
assert valid_msgpacked_dict(data, len(b"garbage"))
523+
assert not valid_msgpacked_dict(data, 0)
524+
# the data ends within the key (fixmap header 1 byte, fixstr header 1 byte, key "path" 4 bytes)
525+
assert not valid_msgpacked_dict(data[: len(b"garbage") + 1 + 1 + 3], len(b"garbage"))
526+
assert not valid_msgpacked_dict(data, len(data))
527+
# a memoryview is only indexed, never sliced (see valid_msgpacked_dict)
528+
with memoryview(data) as view:
529+
assert valid_msgpacked_dict(view, len(b"garbage"))
530+
531+
519532
def test_resync_on_item_with_unknown_first_key():
520533
# after damage, an item whose first key is unknown to this borg version is still found as a resync point.
521534
item = {"aaa": 1, "path": "foo"} # "aaa" sorts before all known item keys

0 commit comments

Comments
 (0)