leptris 1.9.311.0-ppc64le-linux → 1.9.311.2-ppc64le-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 253cc4928e73c6151db3426cec6564a0c1e9b7e6d735921210de3498d07c9f80
4
- data.tar.gz: 944c12f01b50a20fb5c909532467488a83dd94554465b1b55126f48f608c2a63
3
+ metadata.gz: ce070835e17a9965be749612ab0780f8920c3b35dcfebb7b860e387be61fe40b
4
+ data.tar.gz: 536cdb4a6c1b5c291b9976ccba0451b5c0bf8f918e332c31f7f1d9caf78ce13f
5
5
  SHA512:
6
- metadata.gz: a459b9b1dd995cd2cb553c0941f1741a308aa585351eaa2cde3034ed4fcdda7f5eb3c5100d58603cc1d8046995892b5d7b7ea723e198277ae03423386aac6146
7
- data.tar.gz: 2ecc975d6e07f68bc76d458ba62e3ba4fffe97c577bb2c7f24e41896729a9049a3bb7cccb0d26d23274dc3a237e40da2ea64ee70a15bf3d082e93a1f5646caf4
6
+ metadata.gz: 5accdd55f05bc3e7a74766881f44b4722f9e1e22a119bb56c68fd28ed5db5989263e4f15332503d735a0a68890333130f40b1ba0b1d5608371d09d1549209b09
7
+ data.tar.gz: 348a370cc638e7b93bed5e3d68d883f373f8ac0436c94ecdaf34b72cacd627e957f63e254fe79f8b353c29dc22f53ba2c80a41a3257cd0dc551cabb71a56e2fb
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
@@ -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);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.311.0"
4
+ VERSION = "1.9.311.2"
5
5
  end
@@ -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
- Freed = Struct.new(:state) # state: :alive | :freed
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
- cross = element.document && !element.document.equal?(self)
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)
@@ -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 (1.9.304,
423
- # #1528): the engine deep-copies them at the splice, so the
424
- # full path's lift must run FIRST and the source original is
425
- # removed after (move semantics).
426
- if node.document && !node.document.equal?(@document)
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 + engine copy), then
459
- # the source original is removed; returns the installed copy
460
- # (self's new next sibling).
461
- if node.document && !node.document.equal?(@document)
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 + engine copy), then
494
- # the source original is removed; returns the installed copy
495
- # (self's new previous sibling).
496
- if node.document && !node.document.equal?(@document)
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? do |n|
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
- # Cross-document adoption (1.9.304, #1528): the engine
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. Route those through the re-resolving path.
675
- cross = node_or_markup.document &&
676
- !node_or_markup.document.equal?(@document)
677
- return append_installed(node_or_markup) if cross
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
Binary file
@@ -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.advance_version
292
+ @document&.advance_version
292
293
  nil
293
294
  end
294
295
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.311.0
4
+ version: 1.9.311.2
5
5
  platform: ppc64le-linux
6
6
  authors:
7
7
  - Ribose Inc.