leptris 1.9.311.0-aarch64-linux → 1.9.311.2-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/ext/leptris/native/native.c +26 -2
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +18 -3
- data/lib/leptris/xml/element.rb +28 -21
- data/lib/leptris/xml/native.so +0 -0
- data/lib/leptris/xml/node.rb +3 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5bdff2edc4dd2718b7e55edc7cb459597f5119a7275439648cb14118510c2b48
|
|
4
|
+
data.tar.gz: d2925ceeb71346cc106c8ed3985e490779a9ba23a8c61b576d8f7b4e9527b7ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 013d7a590fbf1c5e03b2c5b3be4158519f976c30694dc35f45ed5a1e9cfa7e655d421f40751b80ead5a84dda2a8dd1e429991574d50ec29b0db404bd15302cc8
|
|
7
|
+
data.tar.gz: a1c8447bc14ea7636dd3ca945de6c0d64fd79512ff119c53bfd923fa2d4f188f49785c230c5713da8f07bb298c7d41633906134107cf216290a00b7f1d043e4c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
## [1.9.311.2] - 2026-10-06
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Absorbed sources alive at exit no longer double-free the pool
|
|
6
|
+
(#386)**: `Document#absorb`'s handle-only contract was enforced
|
|
7
|
+
only by the explicit `#free` — the C lifetime handle's dfree
|
|
8
|
+
and the no-native Ruby finalizer still called
|
|
9
|
+
`leptris_document_free`, destroying a pool already owned (and
|
|
10
|
+
released) by the absorber. Libraries that pin adopted-source
|
|
11
|
+
documents (moxml's lifetime pins) aborted at exit-time
|
|
12
|
+
finalization (`leptris_pool_destroy` → `free_small_botch`). The
|
|
13
|
+
absorbed flag now lives where every release path reads it: the
|
|
14
|
+
shared `Freed` struct (Ruby finalizer) and the C handle
|
|
15
|
+
(`doc_handle_mark_absorbed`; dfree is handle-only when the pool
|
|
16
|
+
moved). Both paths pinned by subprocess regression specs.
|
|
17
|
+
|
|
18
|
+
## [1.9.311.1] - 2026-10-06
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Docless children now route through the adoption contract
|
|
23
|
+
(#376)**: moxml's created nodes arrive as Node wrappers whose
|
|
24
|
+
#document is nil but whose C node is live in a foreign pool —
|
|
25
|
+
the splice sites' foreign check required a document, so those
|
|
26
|
+
children bypassed the namespace lift and, worse, the returned
|
|
27
|
+
wrapper addressed the ABANDONED source instead of the installed
|
|
28
|
+
in-tree copy (moxml's flows then mutated a node the tree never
|
|
29
|
+
saw). `foreign_child?` now treats document-nil as foreign at
|
|
30
|
+
add_child, prepend_child, add_next_sibling,
|
|
31
|
+
add_previous_sibling, and append_children, so every splice
|
|
32
|
+
returns the installed wrapper. Document#root= routes docless
|
|
33
|
+
roots through the copying ex-face too — the plain set_root of a
|
|
34
|
+
foreign-pool node linked the pointer WITHOUT adopting it into
|
|
35
|
+
the destination pool, so destination-side writes landed in the
|
|
36
|
+
source arena and reads through the root memo missed them.
|
|
37
|
+
Node-level mutations on docless
|
|
38
|
+
wrappers no longer crash the version gate (they memoize
|
|
39
|
+
nothing, so there is no version to advance).
|
|
40
|
+
|
|
1
41
|
## [1.9.311.0] - 2026-10-06
|
|
2
42
|
|
|
3
43
|
### Added
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -2682,15 +2682,21 @@ static VALUE nf_fast_inner_xml(VALUE self, VALUE addr)
|
|
|
2682
2682
|
* Ruby-finalizer path (which stays for LEPTRIS_NO_NATIVE). */
|
|
2683
2683
|
struct doc_handle {
|
|
2684
2684
|
void *doc;
|
|
2685
|
+
/* Pool ownership transferred to an absorber (engine #1548):
|
|
2686
|
+
* the destination's free performs the real release, so this
|
|
2687
|
+
* handle's dfree is HANDLE-ONLY (#386: malloc_zone_error /
|
|
2688
|
+
* free_small_botch when an absorbed source survived to
|
|
2689
|
+
* exit-time finalization after its destination's release). */
|
|
2690
|
+
int absorbed;
|
|
2685
2691
|
};
|
|
2686
2692
|
|
|
2687
2693
|
static void dh_free(void *p)
|
|
2688
2694
|
{
|
|
2689
2695
|
struct doc_handle *h = p;
|
|
2690
|
-
if (h->doc) {
|
|
2696
|
+
if (h->doc && !h->absorbed) {
|
|
2691
2697
|
f_doc_free(h->doc);
|
|
2692
|
-
h->doc = NULL;
|
|
2693
2698
|
}
|
|
2699
|
+
h->doc = NULL;
|
|
2694
2700
|
}
|
|
2695
2701
|
|
|
2696
2702
|
static size_t dh_size(const void *p)
|
|
@@ -2888,6 +2894,22 @@ static VALUE nf_doc_handle_release(VALUE self, VALUE document)
|
|
|
2888
2894
|
return Qnil;
|
|
2889
2895
|
}
|
|
2890
2896
|
|
|
2897
|
+
/* Absorption bookkeeping (#386, engine #1548): Document#absorb
|
|
2898
|
+
* calls this on the SOURCE right after the engine transfers pool
|
|
2899
|
+
* ownership — the handle's dfree becomes handle-only, matching
|
|
2900
|
+
* the Ruby-side Freed flag the no-native finalizer reads. */
|
|
2901
|
+
static VALUE nf_doc_handle_mark_absorbed(VALUE self, VALUE document)
|
|
2902
|
+
{
|
|
2903
|
+
VALUE handle = rb_ivar_get(document, id_iv_doc_handle);
|
|
2904
|
+
(void)self;
|
|
2905
|
+
if (handle != Qnil) {
|
|
2906
|
+
struct doc_handle *h;
|
|
2907
|
+
TypedData_Get_Struct(handle, struct doc_handle, &dh_type, h);
|
|
2908
|
+
h->absorbed = 1;
|
|
2909
|
+
}
|
|
2910
|
+
return Qnil;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2891
2913
|
/* The full Document.create in one dispatch: engine create,
|
|
2892
2914
|
* binding wrapper (ivar-seeded, bypassing initialize), and the
|
|
2893
2915
|
* lifetime handle. Returns Qnil when the engine refuses. */
|
|
@@ -3158,6 +3180,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
|
|
|
3158
3180
|
nf_doc_handle_attach, 1);
|
|
3159
3181
|
rb_define_module_function(m_native, "doc_handle_release",
|
|
3160
3182
|
nf_doc_handle_release, 1);
|
|
3183
|
+
rb_define_module_function(m_native, "doc_handle_mark_absorbed",
|
|
3184
|
+
nf_doc_handle_mark_absorbed, 1);
|
|
3161
3185
|
rb_define_module_function(m_native, "create_binding_document",
|
|
3162
3186
|
nf_create_binding_document, 0);
|
|
3163
3187
|
c_doc_handle = rb_define_class_under(m_xml, "DocHandle", rb_cObject);
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -11,7 +11,9 @@ class Leptris::XML::Document
|
|
|
11
11
|
# the explicit `free` path and the finalizer. This eliminates the
|
|
12
12
|
# double-free that FFI::AutoPointer's release proc caused when
|
|
13
13
|
# `Document#free` was called explicitly and then GC ran.
|
|
14
|
-
|
|
14
|
+
# +absorbed+ (engine #1548, #386): pool ownership moved to an
|
|
15
|
+
# absorber — the finalizer must never call the engine free.
|
|
16
|
+
Freed = Struct.new(:state, :absorbed) # state: :alive | :freed
|
|
15
17
|
|
|
16
18
|
# Mutation version: advanced by every data mutation (via
|
|
17
19
|
# Node#ensure_writable!, root=, add_pi). Node memos stamp the
|
|
@@ -286,7 +288,7 @@ class Leptris::XML::Document
|
|
|
286
288
|
|
|
287
289
|
def self.finalizer(address, freed)
|
|
288
290
|
proc do
|
|
289
|
-
next if freed.state == :freed
|
|
291
|
+
next if freed.state == :freed || freed.absorbed
|
|
290
292
|
freed.state = :freed
|
|
291
293
|
Leptris::XML::FFI.leptris_document_free(::FFI::Pointer.new(address))
|
|
292
294
|
end
|
|
@@ -388,7 +390,12 @@ class Leptris::XML::Document
|
|
|
388
390
|
# the foreign subtree into this document's pool and reports the
|
|
389
391
|
# installed element — a fresh handle distinct from the source
|
|
390
392
|
# pointer. Same-document installs keep the native fast path.
|
|
391
|
-
|
|
393
|
+
# Docless roots count as foreign (#376): a plain set_root of a
|
|
394
|
+
# foreign-pool node links the pointer WITHOUT adopting it into
|
|
395
|
+
# this pool — writes through this document land in the source
|
|
396
|
+
# arena and reads through the root memo miss them. The ex-face
|
|
397
|
+
# copies properly.
|
|
398
|
+
cross = Leptris::XML::Element.foreign_to?(element, self)
|
|
392
399
|
installed_ptr = nil
|
|
393
400
|
if cross
|
|
394
401
|
out = ::FFI::MemoryPointer.new(:pointer)
|
|
@@ -586,8 +593,16 @@ class Leptris::XML::Document
|
|
|
586
593
|
# objects by hand): the absorbed document's #free skips the
|
|
587
594
|
# engine release, and splice seams skip source-side removals
|
|
588
595
|
# because absorbed splices MOVE the node (nothing to remove).
|
|
596
|
+
# The flag is mirrored into every release path (#386): the
|
|
597
|
+
# shared Freed struct (the no-native Ruby finalizer reads it)
|
|
598
|
+
# and the C lifetime handle (dh_free must be handle-only when
|
|
599
|
+
# the pool belongs to the absorber).
|
|
589
600
|
def mark_absorbed_into(destination)
|
|
590
601
|
@absorbed_into = destination
|
|
602
|
+
@freed.absorbed = true
|
|
603
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
604
|
+
Leptris::XML::Native.doc_handle_mark_absorbed(self)
|
|
605
|
+
end
|
|
591
606
|
end
|
|
592
607
|
|
|
593
608
|
def absorbed_into?(destination)
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -389,6 +389,15 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
389
389
|
!Leptris::XML::Native.ns_lift_needed?(node.c_ptr.address)
|
|
390
390
|
end
|
|
391
391
|
|
|
392
|
+
# A node is foreign to +document+ when it belongs to another one
|
|
393
|
+
# — or to NONE (#376): moxml's created nodes are live C nodes
|
|
394
|
+
# wrapped without a document, and they splice by copy exactly
|
|
395
|
+
# like attached-foreign ones. One definition of "foreign" for
|
|
396
|
+
# the element splices and Document#root=.
|
|
397
|
+
def self.foreign_to?(node, document)
|
|
398
|
+
node.document.nil? || !node.document.equal?(document)
|
|
399
|
+
end
|
|
400
|
+
|
|
392
401
|
def self.lift_namespaces_for_adoption(node, target_scope)
|
|
393
402
|
return unless node.is_a?(Leptris::XML::Element)
|
|
394
403
|
# Declarations the node already carries (its own definitions —
|
|
@@ -419,11 +428,11 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
419
428
|
# C-bound insert (TODO.perf/14): gates + predicate + version
|
|
420
429
|
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
421
430
|
# the namespace lift — the full path below handles it.
|
|
422
|
-
# Cross-document nodes skip the fast path entirely
|
|
423
|
-
# #1528): the engine deep-copies them at the
|
|
424
|
-
# full path's lift must run FIRST and the source
|
|
425
|
-
# removed after (move semantics).
|
|
426
|
-
if
|
|
431
|
+
# Cross-document and docless nodes skip the fast path entirely
|
|
432
|
+
# (1.9.304, #1528; #376): the engine deep-copies them at the
|
|
433
|
+
# splice, so the full path's lift must run FIRST and the source
|
|
434
|
+
# original is removed after (move semantics).
|
|
435
|
+
if Leptris::XML::Element.foreign_to?(node, @document)
|
|
427
436
|
ensure_writable!
|
|
428
437
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
429
438
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
@@ -455,10 +464,10 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
455
464
|
# C-bound insert (TODO.perf/14): gates + predicate + version
|
|
456
465
|
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
457
466
|
# the namespace lift — the full path below handles it.
|
|
458
|
-
# Cross-document: full path first (lift +
|
|
459
|
-
# the source original is removed; returns
|
|
460
|
-
# (self's new next sibling).
|
|
461
|
-
if
|
|
467
|
+
# Cross-document and docless (#376): full path first (lift +
|
|
468
|
+
# engine copy), then the source original is removed; returns
|
|
469
|
+
# the installed copy (self's new next sibling).
|
|
470
|
+
if Leptris::XML::Element.foreign_to?(node, @document)
|
|
462
471
|
ensure_writable!
|
|
463
472
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
464
473
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
@@ -490,10 +499,10 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
490
499
|
# C-bound insert (TODO.perf/14): gates + predicate + version
|
|
491
500
|
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
492
501
|
# the namespace lift — the full path below handles it.
|
|
493
|
-
# Cross-document: full path first (lift +
|
|
494
|
-
# the source original is removed; returns
|
|
495
|
-
# (self's new previous sibling).
|
|
496
|
-
if
|
|
502
|
+
# Cross-document and docless (#376): full path first (lift +
|
|
503
|
+
# engine copy), then the source original is removed; returns
|
|
504
|
+
# the installed copy (self's new previous sibling).
|
|
505
|
+
if Leptris::XML::Element.foreign_to?(node, @document)
|
|
497
506
|
ensure_writable!
|
|
498
507
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
499
508
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
@@ -551,9 +560,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
551
560
|
nodes.each { |n| add_child(n) }
|
|
552
561
|
return
|
|
553
562
|
end
|
|
554
|
-
cross = nodes.any?
|
|
555
|
-
n.document && !n.document.equal?(@document)
|
|
556
|
-
end
|
|
563
|
+
cross = nodes.any? { |n| Leptris::XML::Element.foreign_to?(n, @document) }
|
|
557
564
|
if native_fast_children?
|
|
558
565
|
addrs = nodes.map { |n| n.c_ptr.address }
|
|
559
566
|
done = Leptris::XML::Native.append_binding_children(
|
|
@@ -668,13 +675,13 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
668
675
|
ensure_writable!
|
|
669
676
|
case node_or_markup
|
|
670
677
|
when Leptris::XML::Node
|
|
671
|
-
#
|
|
678
|
+
# Foreign-child adoption (1.9.304, #1528): the engine
|
|
672
679
|
# deep-copies foreign nodes at the splice — the returned
|
|
673
680
|
# wrapper must be the INSTALLED copy, not the detached
|
|
674
|
-
# source.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
return append_installed(node_or_markup) if
|
|
681
|
+
# source. Docless wrappers count as foreign (#376): moxml's
|
|
682
|
+
# created nodes carry live C pointers with no owning
|
|
683
|
+
# document, and their callers expect the in-tree handle.
|
|
684
|
+
return append_installed(node_or_markup) if Leptris::XML::Element.foreign_to?(node_or_markup, @document)
|
|
678
685
|
# C-bound append (TODO.perf/08-09): one dispatch runs the
|
|
679
686
|
# readonly/liveness gates, the provable no-op lift
|
|
680
687
|
# predicate, the version bump, and the engine append. Qnil
|
data/lib/leptris/xml/native.so
CHANGED
|
Binary file
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -281,14 +281,15 @@ class Leptris::XML::Node
|
|
|
281
281
|
# passes through this gate, so it is where the document's mutation
|
|
282
282
|
# version advances — the invalidation behind writable-document
|
|
283
283
|
# memoization. Bumping before the C call is conservative: a failed
|
|
284
|
-
# mutation merely discards memos.
|
|
284
|
+
# mutation merely discards memos. Docless wrappers (#376) mutate
|
|
285
|
+
# fine — they memoize nothing, so there is no version to advance.
|
|
285
286
|
def ensure_writable!
|
|
286
287
|
ensure_alive!
|
|
287
288
|
if readonly_document?
|
|
288
289
|
raise Leptris::XML::ReadOnlyError,
|
|
289
290
|
"document is readonly — mutation attempted on #{inspect}"
|
|
290
291
|
end
|
|
291
|
-
@document
|
|
292
|
+
@document&.advance_version
|
|
292
293
|
nil
|
|
293
294
|
end
|
|
294
295
|
|