Skip to content

unix-ffi/re: Fix the PCRE2 memory leaks. - #1153

Draft
klukonin wants to merge 4 commits into
micropython:masterfrom
klukonin:fix/re-memory-leaks
Draft

unix-ffi/re: Fix the PCRE2 memory leaks.#1153
klukonin wants to merge 4 commits into
micropython:masterfrom
klukonin:fix/re-memory-leaks

Conversation

@klukonin

Copy link
Copy Markdown

unix-ffi/re never frees anything it gets from PCRE2. Every match leaks the
match data block, and every compiled pattern leaks as well, so a program or module (such as json) that
uses re module in a loop grows without bound.

Measured on a ports/unix build (1.30.0-preview) against libpcre2-8, resident
set size around 2x5000 calls:

entry point before after
Pattern.search(), match / no match 4671 / 4384 B per call 0
Pattern.match() / sub() / split() / findall() 4674 / 9025 / 9024 / 13728 0
re.search() / re.match() 4848 B per call 0
re.sub() / re.split() / re.findall() 17824 / 17823 / 14629 0
re.compile(), same pattern 176 B per call 0
re.compile(), distinct patterns 176 B per call unchanged past the cache limit

70k operations grew the process from 4 MB to 367 MB before.
With the fixes it is flat now.

Every call to search() allocated a match data block with
pcre2_match_data_create_from_pattern() and never freed it again, leaking
a few kilobytes per call, on the no-match path as well.  Free it once the
offsets have been copied out of it.

The module level functions compile a pattern that the caller never gets
to see, and that was leaked as well.  Free it when the call is done; the
match object that is returned does not refer to it.

Note that a pattern returned by re.compile() still has to be kept alive
by the caller and cannot be released automatically, because MicroPython
does not run __del__ on instances of Python classes.

Signed-off-by: Kirill Lukonin (Evil Wireless Man) <klukonin@gmail.com>
The error code and the error offset were passed as bytes(4).  Such
objects are immutable, and the error offset is a PCRE2_SIZE, which is 8
bytes on a 64-bit target, so a failing compile wrote 4 bytes past the end
of the buffer.  Use writable arrays of the right size instead, and report
the values in the assertion.

Signed-off-by: Kirill Lukonin (Evil Wireless Man) <klukonin@gmail.com>
The test measures the resident set size around a few thousand calls and
fails if it keeps growing.  It covers every entry point that makes PCRE2
allocate: matching with a compiled pattern, the module level functions,
and compiling itself, including a pattern that fails to compile.

Without the preceding fixes it reports between 4.6 and 17.8 kilobytes of
growth per call, depending on the entry point.

Signed-off-by: Kirill Lukonin (Evil Wireless Man) <klukonin@gmail.com>
Every call to re.search(), and to the functions next to it, compiled the
pattern it was given.  Keep the compiled patterns in a small cache
instead, the way CPython does, so that using the same pattern again does
not compile it a second time.  compile() returns the cached pattern as
well, so re.compile(p) is re.compile(p), as it is in CPython.  Matching
against a repeated pattern gets about twice as fast, compiling one about
eight times.

Because MicroPython cannot release a compiled pattern by itself, the
cache also decides what is kept: a cached pattern stays for the lifetime
of the program, and a pattern that this module compiled for its own use
is freed again afterwards.

The cache owns what it holds and never evicts it.  A pattern that is
still in use, by the caller or by a call further up the stack, must not
be freed underneath it, which a replacement callback passed to sub() can
otherwise trigger.  The cache is bounded instead: once it is full,
further patterns are compiled and, where this module owns them, freed
again after use.

Signed-off-by: Kirill Lukonin (Evil Wireless Man) <klukonin@gmail.com>
@Josverl Josverl added the enhancement Feature requests, new feature implementations label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Feature requests, new feature implementations

2 participants