Skip to content

Commit 88952d0

Browse files
committed
Reject reuse of finished pyexpat parsers
Return the Expat finished error on later input after successful final parsing. Check ParseFile after resolving read but before calling it. Assisted-by: OpenAI Codex:model version not exposed
1 parent 18fc82b commit 88952d0

1 file changed

Lines changed: 25 additions & 9 deletions

File tree

‎crates/stdlib/src/pyexpat.rs‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,12 @@ mod _pyexpat {
264264
// drain.
265265
#[pytraverse(skip)]
266266
busy: AtomicBool,
267-
// Set once EndDocument has been observed, so further Parse() calls
268-
// become harmless no-ops instead of starting a new document.
267+
// Prevent creating another backend after finalization or fork failure.
269268
#[pytraverse(skip)]
270269
finished: AtomicBool,
270+
// Distinguish successful completion from the existing failure paths.
271+
#[pytraverse(skip)]
272+
completed: AtomicBool,
271273
#[pytraverse(skip)]
272274
namespace_stack: PyRwLock<Vec<(xml::namespace::Namespace, Vec<String>)>>,
273275
#[pytraverse(skip)]
@@ -946,6 +948,7 @@ mod _pyexpat {
946948
backend: PyRwLock::new(None),
947949
busy: AtomicBool::new(false),
948950
finished: AtomicBool::new(false),
951+
completed: AtomicBool::new(false),
949952
namespace_stack: PyRwLock::new(Vec::new()),
950953
reparse_deferral_enabled: AtomicBool::new(true),
951954
parameter_entity_parsing: AtomicI32::new(XML_PARAM_ENTITY_PARSING_NEVER),
@@ -1636,6 +1639,7 @@ mod _pyexpat {
16361639
continue;
16371640
}
16381641
ParserMsg::Finished => {
1642+
self.completed.store(true, Ordering::SeqCst);
16391643
self.finished.store(true, Ordering::SeqCst);
16401644
self.teardown_backend();
16411645
return Ok(());
@@ -1751,16 +1755,25 @@ mod _pyexpat {
17511755
}
17521756
}
17531757
if isfinal {
1754-
// The caller has declared there is no more data coming, so
1755-
// (whether or not this re-parse actually reached
1756-
// `EndDocument`) further Parse()/ParseFile() calls become
1757-
// no-ops, matching libexpat/`Backend::Threaded`.
1758+
// Remember successful completion separately from errors;
1759+
// both close this backend, but only success means the parser
1760+
// should report XML_ERROR_FINISHED on a later call.
1761+
if result.is_ok() {
1762+
self.completed.store(true, Ordering::SeqCst);
1763+
}
17581764
self.finished.store(true, Ordering::SeqCst);
17591765
self.teardown_backend();
17601766
}
17611767
result
17621768
}
17631769

1770+
fn check_completed(&self, vm: &VirtualMachine) -> PyResult<()> {
1771+
if self.completed.load(Ordering::SeqCst) {
1772+
return Err(self.handler_error(vm, 36, "parsing finished")?);
1773+
}
1774+
Ok(())
1775+
}
1776+
17641777
/// Feed another chunk of the document to the (possibly newly
17651778
/// created) backend and dispatch whatever events that unblocks.
17661779
/// `Parse()` and `ParseFile()` may each be called several times
@@ -1804,9 +1817,8 @@ mod _pyexpat {
18041817
)));
18051818
}
18061819
self.started.store(true, Ordering::Relaxed);
1820+
self.check_completed(vm)?;
18071821
if self.finished.load(Ordering::SeqCst) {
1808-
// The document already ended; further data is ignored, same
1809-
// as libexpat happily no-oping once XML_Parse has seen EOF.
18101822
return Ok(());
18111823
}
18121824
{
@@ -1870,7 +1882,11 @@ mod _pyexpat {
18701882

18711883
#[pymethod(name = "ParseFile")]
18721884
fn parse_file(zelf: &Py<Self>, file: PyObjectRef, vm: &VirtualMachine) -> PyResult<i32> {
1873-
let read_res = vm.call_method(&file, "read", ())?;
1885+
let read = vm
1886+
.get_attribute_opt(&file, "read")?
1887+
.ok_or_else(|| vm.new_type_error("argument must have 'read' attribute"))?;
1888+
zelf.check_completed(vm)?;
1889+
let read_res = read.call((), vm)?;
18741890
let bytes_like = ArgBytesLike::try_from_object(vm, read_res)?;
18751891
let buf = bytes_like.borrow_buf().to_vec();
18761892
// `file.read()` with no argument reads to EOF, so this chunk is

0 commit comments

Comments
 (0)