Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## [Unreleased]

## [0.3.21] (2026-08-02)

### Fixed

* `Node::get_namespaces` leaked the `xmlGetNsList` result array on every
call, since the crate's beginning: the list is a caller-freed malloc'd
array of `xmlNsPtr` (the namespaces belong to the document); the
historical attempt freed it with `xmlFreeNs` — the first namespace struct
— segfaulted, and was commented out. Measured downstream: 2,429 lost
blocks over 30 documents, a leading term of a ~150 KB/page RSS climb
across a 115,000-page render. Freed with the per-target allocator shim
(MSVC-safe).

## [0.3.20] (2026-08-02)

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "libxml"
version = "0.3.20"
version = "0.3.21"
edition = "2024"
rust-version = "1.88"
authors = ["Andreas Franzén <andreas@devil.se>", "Deyan Ginev <deyan.ginev@gmail.com>","Jan Frederik Schaefer <j.schaefer@jacobs-university.de>"]
Expand Down
26 changes: 13 additions & 13 deletions src/tree/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,18 @@ impl Node {
}
}

/// Gets a list of namespaces associated with this node
/// Gets a list of namespaces associated with this node.
///
/// `xmlGetNsList` mallocs an ARRAY of `xmlNsPtr` that the CALLER must
/// free — the namespaces it points at belong to the document and must NOT
/// be freed. The historical TODO here tried `xmlFreeNs(list)` (frees the
/// first namespace struct — hence the remembered segfault) and gave up,
/// leaking the array on EVERY call: measured downstream at 2,429 lost
/// blocks over 30 documents (valgrind), a leading term of a ~150 KB/page
/// RSS climb across a 115,000-page render whose materialization path calls
/// this per node. The array is freed with the crate's per-target
/// allocator shim (`bindgenFree` — NOT the `xmlFree` global, which is not
/// a linkable symbol on MSVC).
pub fn get_namespaces(&self, doc: &Document) -> Vec<Namespace> {
let list_ptr_raw = unsafe { xmlGetNsList(doc.doc_ptr(), self.node_ptr()) };
if list_ptr_raw.is_null() {
Expand All @@ -931,19 +942,8 @@ impl Node {
namespaces.push(Namespace { ns_ptr: *ptr_iter });
ptr_iter = ptr_iter.add(1);
}
/* TODO: valgrind suggests this technique isn't sufficiently fluent:
==114895== Conditional jump or move depends on uninitialised value(s)
==114895== at 0x4E9962F: xmlFreeNs (in /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.4)
==114895== by 0x195CE8: libxml::tree::Node::get_namespaces (tree.rs:723)
==114895== by 0x12E7B6: base_tests::can_work_with_namespaces (base_tests.rs:537)

DG: I could not improve on this state without creating memory leaks after ~1 hour, so I am
marking it as future work.
*/
/* TODO: How do we properly deallocate here? The approach bellow reliably segfaults tree_tests on 1 thread */
// println!("\n-- xmlfreens on : {:?}", list_ptr_raw);
// xmlFreeNs(list_ptr_raw as xmlNsPtr);
}
crate::c_helpers::bindgenFree(list_ptr_raw as *mut std::os::raw::c_void);
namespaces
}
}
Expand Down
Loading