Skip to content

gh-158583: Fix uninitialized memory read in bytes.fromhex() - #158584

Open
vstinner wants to merge 5 commits into
python:mainfrom
vstinner:bytes_fromhex
Open

vstinner wants to merge 5 commits into
python:mainfrom
vstinner:bytes_fromhex

Conversation

@vstinner

@vstinner vstinner commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

@vstinner vstinner added skip news needs backport to 3.14 bugs and security fixes needs backport to 3.15 pre-release feature fixes, bugs and security fixes labels Oct 1, 2026
@vstinner

vstinner commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

cc @StanFromIreland

bytes.fromhex() was modified in Python 3.14 to accept more types; Python 3.13 only accepts str. So the undefined behavior was introduced in Python 3.14. So far, before me running Valgrind, nobody reported the issue. So I'm not sure if it's needed to add a Changelog entry.

The modified code is tested by BytesTest.test_fromhex() and ByteArrayTest.test_fromhex() of test_bytes. For example, running the test in Valgrind reports the issue.

@vstinner

vstinner commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

In Python 3.13, reading str[hexlen] is fine because str object always allocate a trailing null character: https://docs.python.org/dev/c-api/unicode.html.

The internal buffer always includes an extra trailing null character for compatibility with null terminated C strings.

@vstinner

vstinner commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

I'm also curious why only Valgrind reports the issue. In the CI, we are running the Python test suite on Python built with Address Sanitizer. This build doesn't detect usage of uninitialized memory?

@StanFromIreland
StanFromIreland self-requested a review October 2, 2026 07:08
@encukou

encukou commented Oct 2, 2026

Copy link
Copy Markdown
Member

Apparently, when array.array() created from bytes, it's overallocated. One created from a list gives me a crash under ASAN; could you add a test with it?

bytes.fromhex(array.array('B', [ord('2')] * 5))

@StanFromIreland

StanFromIreland commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

In the CI, we are running the Python test suite on Python built with Address Sanitizer. This build doesn't detect usage of uninitialized memory?

We only run with ASan and UBSan, however neither track uninitialised memory. We'd have to run with MSan.

Edit: I went off on a little tangent, but I wrote a patch to add MSan to the CI: #158625

Comment thread Objects/bytesobject.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dead logic now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh well spotted! I removed the dead code.

@StanFromIreland

Copy link
Copy Markdown
Member

Also, I think we should add an entry, it's a bug fix after all.

@vstinner

vstinner commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

Apparently, when array.array() created from bytes, it's overallocated. One created from a list gives me a crash under ASAN

Oh, implementation details can be very surprising sometimes :-D

could you add a test with it?

Sure, I added two tests to test the two modified code paths, using an array created from a list.

I tested manually that the two tests are detected by Valgrind without the fix:

test_fromhex_ub (test.test_bytes.BytesTest.test_fromhex_ub) ... 

==2131686== Invalid read of size 1
==2131686==    at 0x497DA4: _PyBytes_FromHex (bytesobject.c:2705)
==2131686==    by 0x497E62: bytes_fromhex_impl (bytesobject.c:2635)
==2131686==    by 0x497EA7: bytes_fromhex (bytesobject.c.h:1261)
==2131686==    by 0x4F51FC: cfunction_vectorcall_O (methodobject.c:535)
==2131686==    by 0x49BFED: _PyObject_VectorcallTstate (pycore_call.h:149)
==2131686==    by 0x49C0B5: PyObject_Vectorcall (call.c:327)
==2131686==    by 0x5B1597: _Py_VectorCallInstrumentation_StackRefSteal (ceval.c:770)
==2131686==    by 0x5B8723: _PyEval_EvalFrameDefault (generated_cases.c.h:1906)
==2131686==    by 0x5D03B6: _PyEval_EvalFrame (pycore_ceval.h:122)
==2131686==    by 0x5D0581: _PyEval_Vector (ceval.c:2176)
==2131686==    by 0x49BCD9: _PyFunction_Vectorcall (call.c:413)
==2131686==    by 0x49BFED: _PyObject_VectorcallTstate (pycore_call.h:149)
==2131686==  Address 0x14630416 is 0 bytes after a block of size 6 alloc'd
==2131686==    at 0x4841AE6: malloc (vg_replace_malloc.c:447)
==2131686==    by 0x4FFD6A: _PyMem_RawMalloc (obmalloc.c:66)
==2131686==    by 0x514579: PyMem_Malloc (obmalloc.c:1257)
==2131686==    by 0x158A7FBB: newarrayobject_untracked (arraymodule.c:793)
==2131686==    by 0x158A9E49: array_new (arraymodule.c:3003)
==2131686==    by 0x5309ED: type_call (typeobject.c:2442)
==2131686==    by 0x49BE61: _PyObject_MakeTpCall (call.c:242)
==2131686==    by 0x49C067: _PyObject_VectorcallTstate (pycore_call.h:147)
==2131686==    by 0x49C0B5: PyObject_Vectorcall (call.c:327)
==2131686==    by 0x5B1597: _Py_VectorCallInstrumentation_StackRefSteal (ceval.c:770)
==2131686==    by 0x5B8723: _PyEval_EvalFrameDefault (generated_cases.c.h:1906)
==2131686==    by 0x5D03B6: _PyEval_EvalFrame (pycore_ceval.h:122)
==2131686== 
==2131686== Invalid read of size 1
==2131686==    at 0x497D5D: _PyBytes_FromHex (bytesobject.c:2717)
==2131686==    by 0x497E62: bytes_fromhex_impl (bytesobject.c:2635)
==2131686==    by 0x497EA7: bytes_fromhex (bytesobject.c.h:1261)
==2131686==    by 0x4F51FC: cfunction_vectorcall_O (methodobject.c:535)
==2131686==    by 0x49BFED: _PyObject_VectorcallTstate (pycore_call.h:149)
==2131686==    by 0x49C0B5: PyObject_Vectorcall (call.c:327)
==2131686==    by 0x5B1597: _Py_VectorCallInstrumentation_StackRefSteal (ceval.c:770)
==2131686==    by 0x5B8723: _PyEval_EvalFrameDefault (generated_cases.c.h:1906)
==2131686==    by 0x5D03B6: _PyEval_EvalFrame (pycore_ceval.h:122)
==2131686==    by 0x5D0581: _PyEval_Vector (ceval.c:2176)
==2131686==    by 0x49BCD9: _PyFunction_Vectorcall (call.c:413)
==2131686==    by 0x49BFED: _PyObject_VectorcallTstate (pycore_call.h:149)
==2131686==  Address 0x14728e35 is 0 bytes after a block of size 5 alloc'd
==2131686==    at 0x4841AE6: malloc (vg_replace_malloc.c:447)
==2131686==    by 0x4FFD6A: _PyMem_RawMalloc (obmalloc.c:66)
==2131686==    by 0x514579: PyMem_Malloc (obmalloc.c:1257)
==2131686==    by 0x158A7FBB: newarrayobject_untracked (arraymodule.c:793)
==2131686==    by 0x158A9E49: array_new (arraymodule.c:3003)
==2131686==    by 0x5309ED: type_call (typeobject.c:2442)
==2131686==    by 0x49BE61: _PyObject_MakeTpCall (call.c:242)
==2131686==    by 0x49C067: _PyObject_VectorcallTstate (pycore_call.h:147)
==2131686==    by 0x49C0B5: PyObject_Vectorcall (call.c:327)
==2131686==    by 0x5B1597: _Py_VectorCallInstrumentation_StackRefSteal (ceval.c:770)
==2131686==    by 0x5B8723: _PyEval_EvalFrameDefault (generated_cases.c.h:1906)
==2131686==    by 0x5D03B6: _PyEval_EvalFrame (pycore_ceval.h:122)
==2131686== 

@vstinner

vstinner commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

@StanFromIreland:

Also, I think we should add an entry, it's a bug fix after all.

Ok, I added a Changelog entry. I also removed dead code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting merge needs backport to 3.14 bugs and security fixes needs backport to 3.15 pre-release feature fixes, bugs and security fixes skip news

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants