Title: Use-after-free when a decode hook re-enters `feed()` · Issue #695 · msgpack/msgpack-python · GitHub
Open Graph Title: Use-after-free when a decode hook re-enters `feed()` · Issue #695 · msgpack/msgpack-python
X Title: Use-after-free when a decode hook re-enters `feed()` · Issue #695 · msgpack/msgpack-python
Description: Reported by @oakkaya If a decode hook (object_hook / object_pairs_hook / list_hook / ext_hook) calls .feed() on the same Unpacker while it is still unpacking, append_buffer() may reallocate (PyMem_Free) the internal buffer that the in-pr...
Open Graph Description: Reported by @oakkaya If a decode hook (object_hook / object_pairs_hook / list_hook / ext_hook) calls .feed() on the same Unpacker while it is still unpacking, append_buffer() may reallocate (PyMem_...
X Description: Reported by @oakkaya If a decode hook (object_hook / object_pairs_hook / list_hook / ext_hook) calls .feed() on the same Unpacker while it is still unpacking, append_buffer() may reallocate (PyMem_...
Opengraph URL: https://github.com/msgpack/msgpack-python/issues/695
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Use-after-free when a decode hook re-enters `feed()`","articleBody":"Reported by @oakkaya\n\n---\n\nIf a decode hook (`object_hook` / `object_pairs_hook` / `list_hook` / `ext_hook`) calls `.feed()`\non the **same** `Unpacker` while it is still unpacking, `append_buffer()` may reallocate\n(`PyMem_Free`) the internal buffer that the in-progress `unpack_execute()` is still reading from.\nWhen the hook returns, the parser keeps reading the remaining bytes from the freed buffer →\nuse-after-free. On a stock build this is a hard crash (SIGSEGV).\n\nThe application supplies the (re-entrant) hook; the attacker controls the bytes, which decide when\nthe hook fires and how large the re-entrant feed grows the buffer.\n\n## Reproduction\n```python\nimport struct\nfrom msgpack import Unpacker\n\nup = None\ndef ext_hook(code, data):\n # re-entrant feed on the SAME unpacker, large enough to force a buffer realloc\n up.feed(b\"\\xc0\" * 8_000_000)\n return 0\n\nup = Unpacker(ext_hook=ext_hook, max_buffer_size=64 * 1024 * 1024)\n# array(200): [ ext (fires the re-entrant hook), then 199 more elements ]\nup.feed(b\"\\xdc\" + struct.pack(\"\u003eH\", 200) + b\"\\xd4\\x05A\" + b\"\\x2a\" * 199)\nfor _ in up: # SIGSEGV\n pass\n```\nUnder ASan:\n```\nERROR: AddressSanitizer: heap-use-after-free READ of size 1\n #0 unpack_execute msgpack/unpack_template.h:162\nfreed by: PyMem_Free \u003c- Unpacker.feed -\u003e append_buffer\n```\n\n## Root cause\n`Unpacker._unpack` runs the parser over the internal buffer:\n```python\n ret = execute(\u0026self.ctx, self.buf, self.buf_tail, \u0026self.buf_head)\n```\n`unpack_execute` keeps local `p` / `pe` pointers into `self.buf`. A decode hook is invoked from\n*inside* `execute` (at map/array end or for ext). If the hook calls `up.feed(...)`,\n`append_buffer` (`_unpacker.pyx`) reallocates the buffer:\n```c\n new_buf = \u003cchar*\u003ePyMem_Malloc(new_size)\n ...\n memcpy(new_buf, buf + head, tail - head)\n PyMem_Free(buf) # \u003c-- frees the buffer the outer execute() is reading\n```\nAfter the hook returns, `unpack_execute` continues reading from the now-freed `p`/`pe`.\n\n(The same applies to a `file_like.read()` that re-enters `feed()`/`unpack()`; the unpacker is not\nre-entrant but does not guard against it.)\n\n## Suggested fix (verified)\nAdd a re-entrancy guard so a buffer-mutating call during an active parse fails cleanly instead of\ncorrupting memory. Set a flag around the `execute(...)` call and reject `feed()` while it is set:\n```python\n # field: cdef bint _in_exec (init False in __init__)\n\n def feed(self, next_bytes):\n ...\n if self._in_exec:\n raise RuntimeError(\"Unpacker.feed() called re-entrantly during unpacking\")\n ...\n\n # in _unpack, around the execute call:\n self._in_exec = True\n try:\n ret = execute(\u0026self.ctx, self.buf, self.buf_tail, \u0026self.buf_head)\n finally:\n self._in_exec = False\n```\nVerified: the PoC now raises `RuntimeError` instead of crashing (clean under ASan), and normal\nstreaming (`feed()` between objects), iteration, and the `file_like` path are unaffected\n(`read_from_file` calls `append_buffer` *between* `execute()` calls, where the flag is not set, so\nthere is no false positive). A broader guard could also reject re-entrant `unpack()`/`skip()`.\n","author":{"url":"https://github.com/methane","@type":"Person","name":"methane"},"datePublished":"2026-06-16T11:34:28.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/695/msgpack-python/issues/695"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:7a81f2c6-98a2-402d-54fe-3dd942e3133a |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | CF5C:16BCD3:4816AB0:650209F:6A4F7A21 |
| html-safe-nonce | 5a7736afb578dae392f0b4fda6eed1b5aea9eaadc681e589cd7b8b1a40e80b65 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDRjVDOjE2QkNEMzo0ODE2QUIwOjY1MDIwOUY6NkE0RjdBMjEiLCJ2aXNpdG9yX2lkIjoiNTM5ODQ5MTYyNzYwNDkwMDM4NSIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 3c8f091b1d97143a52d35cbdb280bd716fc4ec5d01287dcc0dc4d4c7d6cf639e |
| hovercard-subject-tag | issue:4673759068 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/msgpack/msgpack-python/695/issue_layout |
| twitter:image | https://opengraph.githubassets.com/99033b2fae244ba539429907c42de18f94e50193c4c37508118a3761b99ee5a8/msgpack/msgpack-python/issues/695 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/99033b2fae244ba539429907c42de18f94e50193c4c37508118a3761b99ee5a8/msgpack/msgpack-python/issues/695 |
| og:image:alt | Reported by @oakkaya If a decode hook (object_hook / object_pairs_hook / list_hook / ext_hook) calls .feed() on the same Unpacker while it is still unpacking, append_buffer() may reallocate (PyMem_... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | methane |
| hostname | github.com |
| expected-hostname | github.com |
| None | b92d11c0aa4a77d54ef4af1078b6a15fb5a70a215b30c4ecf28889d5a8e656d9 |
| turbo-cache-control | no-preview |
| go-import | github.com/msgpack/msgpack-python git https://github.com/msgpack/msgpack-python.git |
| octolytics-dimension-user_id | 198264 |
| octolytics-dimension-user_login | msgpack |
| octolytics-dimension-repository_id | 2242705 |
| octolytics-dimension-repository_nwo | msgpack/msgpack-python |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 2242705 |
| octolytics-dimension-repository_network_root_nwo | msgpack/msgpack-python |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 4b249b445842943ed31549e027f57a8ade9881ed |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width