Skip to content

Commit aaae15c

Browse files
gh-155907: Fix error handling in the marshal C API (GH-155909)
Functions reading marshalled data from a FILE* now raise OSError for I/O errors and KeyboardInterrupt for interrupted reading, instead of EOFError. PyMarshal_WriteObjectToFile() and PyMarshal_WriteLongToFile() now detect I/O errors and interrupted writing instead of ignoring them. PyMarshal_WriteObjectToFile() now also sets the error indicator if the value cannot be marshalled. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7d71b3e commit aaae15c

6 files changed

Lines changed: 183 additions & 48 deletions

File tree

Doc/c-api/marshal.rst

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ Numeric values are stored with the least significant byte first.
1616
The module supports several versions of the data format; see
1717
the :py:mod:`Python module documentation <marshal>` for details.
1818

19+
The following exceptions can be raised by these functions:
20+
:exc:`ValueError` if the value cannot be marshalled,
21+
:exc:`ValueError` or :exc:`TypeError` if the data is malformed,
22+
:exc:`EOFError` if the end of the data is reached before the value is complete,
23+
:exc:`OSError` if reading from or writing to a :c:expr:`FILE*` fails,
24+
:exc:`KeyboardInterrupt` if reading or writing is interrupted by a signal,
25+
and :exc:`MemoryError` if memory allocation fails.
26+
27+
.. versionchanged:: next
28+
Previously, in functions taking a :c:expr:`FILE*`,
29+
the reading functions raised :exc:`EOFError`
30+
instead of :exc:`OSError` and :exc:`KeyboardInterrupt`,
31+
and the writing functions ignored I/O errors and interruptions.
32+
1933
.. c:macro:: Py_MARSHAL_VERSION
2034
2135
The current format version. See :py:data:`marshal.version`.
@@ -42,6 +56,8 @@ the :py:mod:`Python module documentation <marshal>` for details.
4256
Return a bytes object containing the marshalled representation of *value*.
4357
*version* indicates the file format.
4458
59+
On error, raises an exception and returns ``NULL``.
60+
4561
4662
The following functions allow marshalled values to be read back in.
4763
@@ -52,8 +68,7 @@ The following functions allow marshalled values to be read back in.
5268
for reading. Only a 32-bit value can be read in using this function,
5369
regardless of the native size of :c:expr:`long`.
5470
55-
On error, sets the appropriate exception (:exc:`EOFError`) and returns
56-
``-1``.
71+
On error, raises an exception and returns ``-1``.
5772
5873
5974
.. c:function:: int PyMarshal_ReadShortFromFile(FILE *file)
@@ -62,17 +77,15 @@ The following functions allow marshalled values to be read back in.
6277
for reading. Only a 16-bit value can be read in using this function,
6378
regardless of the native size of :c:expr:`short`.
6479
65-
On error, sets the appropriate exception (:exc:`EOFError`) and returns
66-
``-1``.
80+
On error, raises an exception and returns ``-1``.
6781
6882
6983
.. c:function:: PyObject* PyMarshal_ReadObjectFromFile(FILE *file)
7084
7185
Return a Python object from the data stream in a :c:expr:`FILE*` opened for
7286
reading.
7387
74-
On error, sets the appropriate exception (:exc:`EOFError`, :exc:`ValueError`
75-
or :exc:`TypeError`) and returns ``NULL``.
88+
On error, raises an exception and returns ``NULL``.
7689
7790
7891
.. c:function:: PyObject* PyMarshal_ReadLastObjectFromFile(FILE *file)
@@ -85,15 +98,13 @@ The following functions allow marshalled values to be read back in.
8598
file. Only use this variant if you are certain that you won't be reading
8699
anything else from the file.
87100
88-
On error, sets the appropriate exception (:exc:`EOFError`, :exc:`ValueError`
89-
or :exc:`TypeError`) and returns ``NULL``.
101+
On error, raises an exception and returns ``NULL``.
90102
91103
92104
.. c:function:: PyObject* PyMarshal_ReadObjectFromString(const char *data, Py_ssize_t len)
93105
94106
Return a Python object from the data stream in a byte buffer
95107
containing *len* bytes pointed to by *data*.
96108
97-
On error, sets the appropriate exception (:exc:`EOFError`, :exc:`ValueError`
98-
or :exc:`TypeError`) and returns ``NULL``.
109+
On error, raises an exception and returns ``NULL``.
99110

Doc/whatsnew/3.16.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,20 @@ Porting to Python 3.16
956956
* :c:func:`PyType_ClearCache` is now a no-op as the type cache is now
957957
implemented per-type. It still returns the current version tag.
958958

959+
* Functions reading marshalled data from a :c:expr:`FILE*`,
960+
such as :c:func:`PyMarshal_ReadObjectFromFile`,
961+
now raise :exc:`OSError` for I/O errors
962+
and :exc:`KeyboardInterrupt` for interrupted reading,
963+
instead of :exc:`EOFError`.
964+
(Contributed by Serhiy Storchaka in :gh:`155907`.)
965+
966+
* :c:func:`PyMarshal_WriteLongToFile` and :c:func:`PyMarshal_WriteObjectToFile`
967+
now set the error indicator for I/O errors and interrupted writing,
968+
instead of ignoring them.
969+
:c:func:`PyMarshal_WriteObjectToFile` now also sets the error indicator
970+
if the value cannot be marshalled.
971+
(Contributed by Serhiy Storchaka in :gh:`155907`.)
972+
959973
Deprecated C APIs
960974
-----------------
961975

Lib/test/test_marshal.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,35 @@ def test_slice(self):
793793
@unittest.skipUnless(_testcapi, 'requires _testcapi')
794794
class CAPI_TestCase(unittest.TestCase, HelperMixin):
795795

796+
def test_read_from_file_error(self):
797+
# A read error is reported as OSError, not EOFError.
798+
# A directory cannot be read (on some platforms it cannot even
799+
# be opened, which is reported as OSError as well).
800+
os.mkdir(os_helper.TESTFN)
801+
self.addCleanup(os_helper.rmdir, os_helper.TESTFN)
802+
for func in (_testcapi.pymarshal_read_short_from_file,
803+
_testcapi.pymarshal_read_long_from_file,
804+
_testcapi.pymarshal_read_object_from_file,
805+
_testcapi.pymarshal_read_last_object_from_file):
806+
with self.subTest(func=func.__name__):
807+
self.assertRaises(OSError, func, os_helper.TESTFN)
808+
809+
@unittest.skipUnless(os.path.exists('/dev/full'), 'requires /dev/full')
810+
def test_write_to_file_error(self):
811+
# A write error is reported as OSError.
812+
# The data is large enough to not fit in the stdio buffer, so that
813+
# the error is detected before the file is closed.
814+
obj = b'x' * 100000
815+
with self.assertRaises(OSError):
816+
_testcapi.pymarshal_write_object_to_file(obj, '/dev/full',
817+
marshal.version)
818+
819+
def test_write_unmarshallable_to_file(self):
820+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
821+
with self.assertRaisesRegex(ValueError, 'unmarshallable object'):
822+
_testcapi.pymarshal_write_object_to_file(object(), os_helper.TESTFN,
823+
marshal.version)
824+
796825
def test_write_long_to_file(self):
797826
for v in range(marshal.version + 1):
798827
_testcapi.pymarshal_write_long_to_file(0x12345678, os_helper.TESTFN, v)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:c:func:`PyMarshal_ReadObjectFromFile` and other functions reading marshalled
2+
data from a :c:expr:`FILE*` now raise :exc:`OSError` for I/O errors and
3+
:exc:`KeyboardInterrupt` for interrupted reading, instead of :exc:`EOFError`.
4+
:c:func:`PyMarshal_WriteObjectToFile` and :c:func:`PyMarshal_WriteLongToFile`
5+
now detect I/O errors and interrupted writing instead of ignoring them.
6+
:c:func:`PyMarshal_WriteObjectToFile` now also sets the error indicator if the
7+
value cannot be marshalled.

Modules/_testcapimodule.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,9 +1436,11 @@ pymarshal_write_long_to_file(PyObject* self, PyObject *args)
14361436
}
14371437

14381438
PyMarshal_WriteLongToFile(value, fp, version);
1439-
assert(!PyErr_Occurred());
14401439

14411440
fclose(fp);
1441+
if (PyErr_Occurred()) {
1442+
return NULL;
1443+
}
14421444
Py_RETURN_NONE;
14431445
}
14441446

@@ -1460,9 +1462,11 @@ pymarshal_write_object_to_file(PyObject* self, PyObject *args)
14601462
}
14611463

14621464
PyMarshal_WriteObjectToFile(obj, fp, version);
1463-
assert(!PyErr_Occurred());
14641465

14651466
fclose(fp);
1467+
if (PyErr_Occurred()) {
1468+
return NULL;
1469+
}
14661470
Py_RETURN_NONE;
14671471
}
14681472

Python/marshal.c

Lines changed: 106 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ module marshal
106106
#define WFERR_NESTEDTOODEEP 2
107107
#define WFERR_NOMEMORY 3
108108
#define WFERR_CODE_NOT_ALLOWED 4
109+
#define WFERR_EXCEPTION_SET 5 /* An exception has already been raised. */
109110

110111
typedef struct {
111112
FILE *fp;
@@ -125,11 +126,32 @@ typedef struct {
125126
*(p)->ptr++ = (c); \
126127
} while(0)
127128

129+
/* Report a failure of the underlying file. An earlier error is not
130+
overwritten. */
131+
static void
132+
w_file_error(WFILE *p)
133+
{
134+
int saved_errno = errno;
135+
if (p->error != WFERR_OK) {
136+
return;
137+
}
138+
p->error = WFERR_EXCEPTION_SET;
139+
if (PyErr_CheckSignals()) {
140+
/* The signal handler has raised an exception. */
141+
return;
142+
}
143+
errno = saved_errno;
144+
PyErr_SetFromErrno(PyExc_OSError);
145+
}
146+
128147
static void
129148
w_flush(WFILE *p)
130149
{
131150
assert(p->fp != NULL);
132-
fwrite(p->buf, 1, p->ptr - p->buf, p->fp);
151+
size_t n = (size_t)(p->ptr - p->buf);
152+
if (fwrite(p->buf, 1, n, p->fp) != n) {
153+
w_file_error(p);
154+
}
133155
p->ptr = p->buf;
134156
}
135157

@@ -182,7 +204,9 @@ w_string(const void *s, Py_ssize_t n, WFILE *p)
182204
}
183205
else {
184206
w_flush(p);
185-
fwrite(s, 1, n, p->fp);
207+
if (fwrite(s, 1, n, p->fp) != (size_t)n) {
208+
w_file_error(p);
209+
}
186210
}
187211
}
188212
else {
@@ -782,11 +806,36 @@ w_clear_refs(WFILE *wf)
782806
}
783807
}
784808

809+
/* Set the exception indicator according to the recorded error. */
810+
static void
811+
w_set_exception(WFILE *p)
812+
{
813+
assert(p->error != WFERR_OK);
814+
switch (p->error) {
815+
case WFERR_NOMEMORY:
816+
PyErr_NoMemory();
817+
break;
818+
case WFERR_NESTEDTOODEEP:
819+
PyErr_SetString(PyExc_ValueError,
820+
"object too deeply nested to marshal");
821+
break;
822+
case WFERR_CODE_NOT_ALLOWED:
823+
PyErr_SetString(PyExc_ValueError,
824+
"marshalling code objects is disallowed");
825+
break;
826+
case WFERR_EXCEPTION_SET:
827+
/* An exception has already been raised. */
828+
assert(PyErr_Occurred());
829+
break;
830+
default:
831+
case WFERR_UNMARSHALLABLE:
832+
PyErr_SetString(PyExc_ValueError,
833+
"unmarshallable object");
834+
break;
835+
}
836+
}
837+
785838
/* version currently has no effect for writing ints. */
786-
/* Note that while the documentation states that this function
787-
* can error, currently it never does. Setting an exception in
788-
* this function should be regarded as an API-breaking change.
789-
*/
790839
void
791840
PyMarshal_WriteLongToFile(long x, FILE *fp, int version)
792841
{
@@ -800,6 +849,9 @@ PyMarshal_WriteLongToFile(long x, FILE *fp, int version)
800849
wf.version = version;
801850
w_long(x, &wf);
802851
w_flush(&wf);
852+
if (wf.error != WFERR_OK) {
853+
w_set_exception(&wf);
854+
}
803855
}
804856

805857
void
@@ -823,6 +875,9 @@ PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
823875
w_object(x, &wf);
824876
w_clear_refs(&wf);
825877
w_flush(&wf);
878+
if (wf.error != WFERR_OK) {
879+
w_set_exception(&wf);
880+
}
826881
}
827882

828883
typedef struct {
@@ -875,6 +930,14 @@ r_string(Py_ssize_t n, RFILE *p)
875930
if (!p->readable) {
876931
assert(p->fp != NULL);
877932
read = fread(p->buf, 1, n, p->fp);
933+
if (read != n) {
934+
assert(read < n);
935+
int saved_errno = errno;
936+
if (!PyErr_CheckSignals() && ferror(p->fp)) {
937+
errno = saved_errno;
938+
PyErr_SetFromErrno(PyExc_OSError);
939+
}
940+
}
878941
}
879942
else {
880943
PyObject *res, *mview;
@@ -887,21 +950,26 @@ r_string(Py_ssize_t n, RFILE *p)
887950
return NULL;
888951

889952
res = _PyObject_CallMethod(p->readable, &_Py_ID(readinto), "N", mview);
890-
if (res != NULL) {
891-
read = PyNumber_AsSsize_t(res, PyExc_ValueError);
892-
Py_DECREF(res);
953+
if (res == NULL) {
954+
return NULL;
955+
}
956+
read = PyNumber_AsSsize_t(res, PyExc_ValueError);
957+
Py_DECREF(res);
958+
if (read == -1 && PyErr_Occurred()) {
959+
return NULL;
960+
}
961+
if (read > n) {
962+
PyErr_Format(PyExc_ValueError,
963+
"read() returned too much data: "
964+
"%zd bytes requested, %zd returned",
965+
n, read);
966+
return NULL;
893967
}
894968
}
895969
if (read != n) {
896970
if (!PyErr_Occurred()) {
897-
if (read > n)
898-
PyErr_Format(PyExc_ValueError,
899-
"read() returned too much data: "
900-
"%zd bytes requested, %zd returned",
901-
n, read);
902-
else
903-
PyErr_SetString(PyExc_EOFError,
904-
"EOF read where not expected");
971+
PyErr_SetString(PyExc_EOFError,
972+
"EOF read where not expected");
905973
}
906974
return NULL;
907975
}
@@ -922,6 +990,15 @@ r_byte(RFILE *p)
922990
if (c != EOF) {
923991
return c;
924992
}
993+
int saved_errno = errno;
994+
if (PyErr_CheckSignals()) {
995+
return EOF;
996+
}
997+
if (ferror(p->fp)) {
998+
errno = saved_errno;
999+
PyErr_SetFromErrno(PyExc_OSError);
1000+
return EOF;
1001+
}
9251002
}
9261003
else {
9271004
const char *ptr = r_string(1, p);
@@ -1850,8 +1927,18 @@ PyMarshal_ReadLastObjectFromFile(FILE *fp)
18501927
if (filesize > 0 && filesize <= REASONABLE_FILE_LIMIT) {
18511928
char* pBuf = (char *)PyMem_Malloc(filesize);
18521929
if (pBuf != NULL) {
1930+
PyObject *v = NULL;
18531931
size_t n = fread(pBuf, 1, (size_t)filesize, fp);
1854-
PyObject* v = PyMarshal_ReadObjectFromString(pBuf, n);
1932+
int saved_errno = errno;
1933+
if (!PyErr_CheckSignals()) {
1934+
if (ferror(fp)) {
1935+
errno = saved_errno;
1936+
PyErr_SetFromErrno(PyExc_OSError);
1937+
}
1938+
else {
1939+
v = PyMarshal_ReadObjectFromString(pBuf, n);
1940+
}
1941+
}
18551942
PyMem_Free(pBuf);
18561943
return v;
18571944
}
@@ -1938,24 +2025,7 @@ _PyMarshal_WriteObjectToString(PyObject *x, int version, int allow_code)
19382025
}
19392026
if (wf.error != WFERR_OK) {
19402027
Py_XDECREF(wf.str);
1941-
switch (wf.error) {
1942-
case WFERR_NOMEMORY:
1943-
PyErr_NoMemory();
1944-
break;
1945-
case WFERR_NESTEDTOODEEP:
1946-
PyErr_SetString(PyExc_ValueError,
1947-
"object too deeply nested to marshal");
1948-
break;
1949-
case WFERR_CODE_NOT_ALLOWED:
1950-
PyErr_SetString(PyExc_ValueError,
1951-
"marshalling code objects is disallowed");
1952-
break;
1953-
default:
1954-
case WFERR_UNMARSHALLABLE:
1955-
PyErr_SetString(PyExc_ValueError,
1956-
"unmarshallable object");
1957-
break;
1958-
}
2028+
w_set_exception(&wf);
19592029
return NULL;
19602030
}
19612031
return wf.str;

0 commit comments

Comments
 (0)