From fb1d2f52b4a0a2de8ceef467d15c75d9703b38e9 Mon Sep 17 00:00:00 2001 From: Generalsimus Date: Wed, 17 Jun 2026 22:39:32 +0400 Subject: [PATCH] Fix ResizableFile.write() crashing when a single resize is insufficient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in ResizableFile: 1. write() used `if` instead of `while` for the resize guard. A single doubling of the mmap may still not be large enough if the value being written is bigger than 2× the current mmap size. The assignment then fails with: IndexError: mmap slice assignment is wrong size The fix loops until the mmap is actually large enough, and also uses max(doubled_size, offset+size) so one iteration always suffices even for arbitrarily large values. 2. Both resize call-sites caught only SystemError as the fallback trigger for __extand(). On Linux with Python 3, mmap.resize() raises OSError (not SystemError) when mremap() fails. The fallback was therefore never reached on Linux, leaving the mmap at its original size and causing the write to crash. Fix catches both exception types. Observed in the wild with Patroni 4.1.3 / pysyncobj 0.3.15 on Ubuntu 24.04 (Python 3.12). When a late-joining raft node receives its first catch-up batch from the leader the batch can be several kilobytes; with a 1 KB initial mmap a single resize to 2 KB is still not enough, hitting bug 1. Bug 2 masks the __extand() fallback on every Linux deployment. --- pysyncobj/journal.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pysyncobj/journal.py b/pysyncobj/journal.py index 44d5bd7..f696ec7 100644 --- a/pysyncobj/journal.py +++ b/pysyncobj/journal.py @@ -90,17 +90,18 @@ def __init__(self, fileName, initialSize = 1024, resizeFactor = 2.0, defaultCont if currSize < initialSize: try: self.__mm.resize(initialSize) - except SystemError: + except (SystemError, OSError): self.__extand(initialSize - currSize) def write(self, offset, values): size = len(values) - currSize = self.__mm.size() - if offset + size > self.__mm.size(): + while offset + size > self.__mm.size(): + currSize = self.__mm.size() + newSize = max(int(currSize * self.__resizeFactor), offset + size) try: - self.__mm.resize(int(self.__mm.size() * self.__resizeFactor)) - except SystemError: - self.__extand(int(self.__mm.size() * self.__resizeFactor) - currSize) + self.__mm.resize(newSize) + except (SystemError, OSError): + self.__extand(newSize - currSize) self.__mm[offset:offset + size] = values def read(self, offset, size):