makiri 0.3.0 → 0.4.0
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/.github/workflows/conformance.yml +22 -0
- data/.github/workflows/libfuzzer.yml +83 -0
- data/.github/workflows/security.yml +88 -3
- data/.github/workflows/valgrind.yml +135 -0
- data/CHANGELOG.md +60 -2
- data/README.md +81 -77
- data/Rakefile +194 -3
- data/ext/makiri/bridge/ruby_string.c +119 -66
- data/ext/makiri/core/mkr_alloc.c +40 -3
- data/ext/makiri/core/mkr_alloc.h +27 -4
- data/ext/makiri/core/mkr_buf.c +13 -3
- data/ext/makiri/core/mkr_buf.h +80 -5
- data/ext/makiri/core/mkr_core.c +143 -0
- data/ext/makiri/core/mkr_core.h +10 -1
- data/ext/makiri/core/mkr_span.h +186 -0
- data/ext/makiri/core/mkr_utf8.c +101 -0
- data/ext/makiri/core/mkr_utf8.h +88 -0
- data/ext/makiri/extconf.rb +104 -9
- data/ext/makiri/fuzz/Makefile +95 -0
- data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
- data/ext/makiri/fuzz/xml_fuzz.c +24 -0
- data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
- data/ext/makiri/glue/glue.h +8 -0
- data/ext/makiri/glue/ruby_doc.c +20 -24
- data/ext/makiri/glue/ruby_html_css.c +58 -12
- data/ext/makiri/glue/ruby_html_mutate.c +11 -6
- data/ext/makiri/glue/ruby_html_node.c +3 -32
- data/ext/makiri/glue/ruby_node.c +39 -0
- data/ext/makiri/glue/ruby_xml.c +198 -16
- data/ext/makiri/glue/ruby_xml_node.c +46 -59
- data/ext/makiri/glue/ruby_xpath.c +4 -4
- data/ext/makiri/lexbor_compat/source_loc.c +14 -16
- data/ext/makiri/lexbor_compat/utf8_input.c +5 -78
- data/ext/makiri/makiri.c +45 -0
- data/ext/makiri/xml/mkr_xml.h +2 -3
- data/ext/makiri/xml/mkr_xml_chars.c +67 -97
- data/ext/makiri/xml/mkr_xml_index.c +169 -0
- data/ext/makiri/xml/mkr_xml_index.h +48 -0
- data/ext/makiri/xml/mkr_xml_mutate.c +63 -121
- data/ext/makiri/xml/mkr_xml_node.c +147 -15
- data/ext/makiri/xml/mkr_xml_node.h +71 -6
- data/ext/makiri/xml/mkr_xml_tree.c +185 -149
- data/ext/makiri/xpath/mkr_css.c +1023 -0
- data/ext/makiri/xpath/mkr_css.h +65 -0
- data/ext/makiri/xpath/mkr_xpath.c +37 -0
- data/ext/makiri/xpath/mkr_xpath.h +13 -0
- data/ext/makiri/xpath/mkr_xpath_eval_body.h +373 -90
- data/ext/makiri/xpath/mkr_xpath_funcs_body.h +249 -231
- data/ext/makiri/xpath/mkr_xpath_internal.h +89 -9
- data/ext/makiri/xpath/mkr_xpath_lex.c +94 -124
- data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +6 -3
- data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
- data/ext/makiri/xpath/mkr_xpath_parse.c +79 -90
- data/ext/makiri/xpath/mkr_xpath_shared.c +40 -24
- data/ext/makiri/xpath/mkr_xpath_value_body.h +50 -24
- data/lib/makiri/cdata_section.rb +1 -3
- data/lib/makiri/comment.rb +1 -3
- data/lib/makiri/document.rb +8 -0
- data/lib/makiri/element.rb +1 -3
- data/lib/makiri/processing_instruction.rb +1 -3
- data/lib/makiri/text.rb +1 -3
- data/lib/makiri/version.rb +1 -1
- data/lib/makiri/xml/builder.rb +263 -0
- data/lib/makiri/xml/node_methods.rb +47 -0
- data/lib/makiri.rb +1 -0
- data/script/check_alloc_failures.rb +266 -0
- data/script/check_c_safety.rb +45 -2
- data/script/check_c_safety_allowlist.yml +19 -0
- data/script/check_leaks.rb +64 -0
- data/script/leaks_harness.rb +64 -0
- data/vendor/lexbor/CMakeLists.txt +6 -0
- data/vendor/lexbor/README.md +12 -0
- data/vendor/lexbor/config.cmake +1 -1
- data/vendor/lexbor/source/lexbor/core/base.h +1 -1
- data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
- data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
- data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
- data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
- data/vendor/lexbor/source/lexbor/html/base.h +1 -1
- data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
- data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
- data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
- data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
- data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
- data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
- data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
- data/vendor/lexbor/source/lexbor/url/base.h +1 -1
- data/vendor/lexbor/source/lexbor/url/url.c +5 -2
- data/vendor/lexbor/source/lexbor/url/url.h +9 -0
- data/vendor/lexbor/version +1 -1
- metadata +19 -1
data/ext/makiri/extconf.rb
CHANGED
|
@@ -28,7 +28,29 @@ abort "Lexbor source not found at #{LEXBOR_SRC}. Did you `git submodule update -
|
|
|
28
28
|
|
|
29
29
|
cmake = find_executable("cmake") or abort "cmake is required to build Lexbor."
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Optionally build the vendored Lexbor itself under AddressSanitizer. This is the
|
|
32
|
+
# ONLY way to catch overflows *inside* Lexbor's bump (mraw) arena: a sub-allocation
|
|
33
|
+
# overrunning into the next one stays within one big malloc'd chunk, so the heap
|
|
34
|
+
# allocator's red-zones (and thus a plain ASan build of just our ext) never see it.
|
|
35
|
+
# Lexbor's own mraw is ASan-aware - with -DLEXBOR_BUILD_WITH_ASAN=ON its CMake
|
|
36
|
+
# defines LEXBOR_HAVE_ADDRESS_SANITIZER, and mraw then unpoisons exactly each
|
|
37
|
+
# allocation and re-poisons the gap, so an intra-arena overrun writes into
|
|
38
|
+
# poisoned memory and ASan reports it. Opt-in (slow full rebuild), only meaningful
|
|
39
|
+
# with MAKIRI_SANITIZE=...address...; drive it via `rake sanitize:lexbor`.
|
|
40
|
+
# vendor/lexbor stays vanilla - this is a build flag, not a source patch.
|
|
41
|
+
sanitize = ENV["MAKIRI_SANITIZE"].to_s.strip
|
|
42
|
+
lexbor_asan = !ENV["MAKIRI_SANITIZE_LEXBOR"].to_s.strip.empty? && sanitize.include?("address")
|
|
43
|
+
lexbor_mode = lexbor_asan ? "asan" : "plain"
|
|
44
|
+
lexbor_stamp = File.join(LEXBOR_DST, ".makiri_build_mode")
|
|
45
|
+
|
|
46
|
+
# Reuse the cached archive only when it was built in the mode we now want; a mode
|
|
47
|
+
# switch (plain <-> asan) forces a rebuild, so a sanitized Lexbor can never leak
|
|
48
|
+
# into a normal build or vice versa.
|
|
49
|
+
have_archive = File.exist?(File.join(LEXBOR_DST, "lib", "liblexbor_static.a"))
|
|
50
|
+
stamp_ok = have_archive && File.exist?(lexbor_stamp) && File.read(lexbor_stamp).strip == lexbor_mode
|
|
51
|
+
unless stamp_ok
|
|
52
|
+
FileUtils.rm_rf(LEXBOR_BLD)
|
|
53
|
+
FileUtils.rm_rf(LEXBOR_DST) if have_archive # drop a wrong-mode install
|
|
32
54
|
FileUtils.mkdir_p(LEXBOR_BLD)
|
|
33
55
|
Dir.chdir(LEXBOR_BLD) do
|
|
34
56
|
cmd = [
|
|
@@ -41,12 +63,15 @@ unless File.exist?(File.join(LEXBOR_DST, "lib", "liblexbor_static.a"))
|
|
|
41
63
|
"-DCMAKE_BUILD_TYPE=Release",
|
|
42
64
|
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON",
|
|
43
65
|
"-DCMAKE_INSTALL_PREFIX=#{LEXBOR_DST}",
|
|
66
|
+
*(lexbor_asan ? ["-DLEXBOR_BUILD_WITH_ASAN=ON"] : []),
|
|
44
67
|
LEXBOR_SRC,
|
|
45
68
|
].shelljoin
|
|
69
|
+
warn "makiri: building vendored Lexbor (mode=#{lexbor_mode})"
|
|
46
70
|
system(cmd) or abort "cmake configure failed for Lexbor."
|
|
47
71
|
system("#{cmake.shellescape} --build . --target install -- -j#{Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 4}") or
|
|
48
72
|
abort "cmake build/install failed for Lexbor."
|
|
49
73
|
end
|
|
74
|
+
File.write(lexbor_stamp, lexbor_mode)
|
|
50
75
|
end
|
|
51
76
|
|
|
52
77
|
$INCFLAGS << " -I#{File.join(LEXBOR_DST, 'include').shellescape}"
|
|
@@ -60,11 +85,35 @@ $LDFLAGS << " #{lexbor_archive.shellescape}"
|
|
|
60
85
|
# Sanitizer build (opt-in): MAKIRI_SANITIZE=address,undefined rake clean compile
|
|
61
86
|
# Then run the suite under the runtime via `rake sanitize` (which preloads the
|
|
62
87
|
# ASan runtime). Sanitizers replace the heap allocator, so even the vendored
|
|
63
|
-
# (uninstrumented) Lexbor's allocations get red-zoned - heap
|
|
64
|
-
# Lexbor
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
# (uninstrumented) Lexbor's allocations get red-zoned - a heap overflow off the
|
|
89
|
+
# END of a Lexbor malloc is caught. Overflows *inside* Lexbor's mraw arena are
|
|
90
|
+
# NOT caught this way (they stay within one malloc'd chunk); for those, also build
|
|
91
|
+
# Lexbor under ASan via MAKIRI_SANITIZE_LEXBOR=1 (see the Lexbor build above and
|
|
92
|
+
# `rake sanitize:lexbor`). _FORTIFY_SOURCE is dropped here because it conflicts
|
|
93
|
+
# with the sanitizer interceptors.
|
|
94
|
+
# Coverage build (opt-in): MAKIRI_COVERAGE=1 instruments OUR sources with clang
|
|
95
|
+
# source-based coverage (the vendored Lexbor is built separately and is NOT
|
|
96
|
+
# instrumented - we measure only the code we write). Run via `rake coverage`,
|
|
97
|
+
# which sets LLVM_PROFILE_FILE and renders an llvm-cov report. -O0 keeps the
|
|
98
|
+
# region map close to the source; _FORTIFY_SOURCE is dropped (it needs -O2).
|
|
99
|
+
coverage = !ENV["MAKIRI_COVERAGE"].to_s.strip.empty?
|
|
100
|
+
|
|
101
|
+
# OOM-injection build (opt-in): MAKIRI_ALLOC_INJECT=1 compiles the core
|
|
102
|
+
# allocation-failure hook (mkr_alloc.h) so `rake oom` can sweep "the nth core
|
|
103
|
+
# allocation fails" over representative workloads and assert every OOM branch
|
|
104
|
+
# fails closed. Debug/test builds only - a normal build carries no hook.
|
|
105
|
+
# Composes with the sanitize/coverage modes below.
|
|
106
|
+
if ENV["MAKIRI_ALLOC_INJECT"].to_s.strip == "1"
|
|
107
|
+
$CFLAGS << " -DMKR_ALLOC_INJECT=1"
|
|
108
|
+
warn "makiri: building with allocation-failure injection (MKR_ALLOC_INJECT)"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if coverage
|
|
112
|
+
$CFLAGS << " -O0 -g -fprofile-instr-generate -fcoverage-mapping"
|
|
113
|
+
$LDFLAGS << " -fprofile-instr-generate"
|
|
114
|
+
$DLDFLAGS << " -fprofile-instr-generate"
|
|
115
|
+
warn "makiri: building with clang source-based coverage"
|
|
116
|
+
elsif sanitize.empty?
|
|
68
117
|
# Security hardening flags. Keep -O2 active so _FORTIFY_SOURCE works.
|
|
69
118
|
$CFLAGS << " -O2"
|
|
70
119
|
$CFLAGS << " -D_FORTIFY_SOURCE=2"
|
|
@@ -72,6 +121,20 @@ else
|
|
|
72
121
|
$CFLAGS << " -O1 -g -fno-omit-frame-pointer -fsanitize=#{sanitize}"
|
|
73
122
|
$LDFLAGS << " -fsanitize=#{sanitize}"
|
|
74
123
|
$DLDFLAGS << " -fsanitize=#{sanitize}"
|
|
124
|
+
if sanitize.include?("address")
|
|
125
|
+
# No ASan *stack* red zones in the ext. CRuby is built with
|
|
126
|
+
# RUBY_SETJMP = __builtin_setjmp, so rb_raise unwinds via __builtin_longjmp,
|
|
127
|
+
# which the ASan runtime does not intercept (no __asan_handle_no_return):
|
|
128
|
+
# any raise crossing an instrumented frame - ours, or Ruby code raising
|
|
129
|
+
# through rb_protect under the evaluator - leaves that frame's stack poison
|
|
130
|
+
# behind, and an interceptor (memcpy & co.) in the uninstrumented interpreter
|
|
131
|
+
# later trips over the stale shadow: a spurious report, which ASan itself
|
|
132
|
+
# then aborts while rendering (asan_thread.cpp kCurrentStackFrameMagic
|
|
133
|
+
# CHECK). Heap red zones, UBSan, and the manual arena poisoning in
|
|
134
|
+
# mkr_xml_node.c are unaffected; only stack-buffer checks are lost.
|
|
135
|
+
$CFLAGS << (RbConfig::CONFIG["CC"] =~ /clang/ || RbConfig::CONFIG["target_os"] =~ /darwin/ ?
|
|
136
|
+
" -mllvm -asan-stack=0" : " --param asan-stack=0")
|
|
137
|
+
end
|
|
75
138
|
warn "makiri: building with -fsanitize=#{sanitize}"
|
|
76
139
|
end
|
|
77
140
|
|
|
@@ -133,9 +196,41 @@ elsif RbConfig::CONFIG["target_os"] =~ /linux/
|
|
|
133
196
|
$DLDFLAGS << " -Wl,--exclude-libs,ALL"
|
|
134
197
|
end
|
|
135
198
|
|
|
136
|
-
# Recursively pick up C sources under ext/makiri
|
|
137
|
-
|
|
199
|
+
# Recursively pick up C sources under ext/makiri/, excluding standalone
|
|
200
|
+
# libFuzzer harnesses. Those define LLVMFuzzerTestOneInput and are linked by
|
|
201
|
+
# ext/makiri/fuzz/Makefile, never into the Ruby extension.
|
|
202
|
+
$srcs = Dir.glob(File.join(EXT_DIR, "**", "*.c"))
|
|
203
|
+
.reject { |f| f.start_with?(File.join(EXT_DIR, "fuzz") + File::SEPARATOR) }
|
|
204
|
+
.map { |f| f.sub("#{EXT_DIR}/", "") }
|
|
138
205
|
$VPATH ||= []
|
|
139
|
-
|
|
206
|
+
# fuzz/ must be excluded here too: after a `rake fuzz:libfuzzer_build`,
|
|
207
|
+
# fuzz/build/{core,xml,xpath}/ hold sanitizer-instrumented .o files, and a
|
|
208
|
+
# VPATH that includes them lets make resolve the extension's object
|
|
209
|
+
# prerequisites there instead of compiling them - breaking the link (or worse,
|
|
210
|
+
# silently mixing differently-flagged objects).
|
|
211
|
+
$VPATH += Dir.glob(File.join(EXT_DIR, "**/"))
|
|
212
|
+
.reject { |d| d.start_with?(File.join(EXT_DIR, "fuzz") + File::SEPARATOR) }
|
|
213
|
+
.map { |d| "$(srcdir)/#{d.sub("#{EXT_DIR}/", "")}".chomp("/") }
|
|
140
214
|
|
|
141
215
|
create_makefile("makiri/makiri")
|
|
216
|
+
|
|
217
|
+
# mkmf's generated Makefile carries NO header dependencies, so editing a header
|
|
218
|
+
# (e.g. a struct layout in an internal .h) recompiles only the .c files whose
|
|
219
|
+
# own timestamps changed - the rest keep their stale layout and the objects
|
|
220
|
+
# silently disagree (ABI mismatch, runtime breakage). Append the coarsest sound
|
|
221
|
+
# rule instead: every object depends on every project header. A header edit
|
|
222
|
+
# then recompiles everything - a few seconds for this ext, and it can never
|
|
223
|
+
# rot (the list regenerates each configure; a header NEW since the last
|
|
224
|
+
# configure is reachable only from .c files edited to include it, which rebuild
|
|
225
|
+
# on their own timestamp). Ruby/Lexbor headers are deliberately excluded: a
|
|
226
|
+
# Ruby upgrade gets a fresh build dir from rake-compiler, and a Lexbor pin
|
|
227
|
+
# change already requires `rake clean:lexbor` (see CLAUDE.md).
|
|
228
|
+
project_headers = Dir.glob(File.join(EXT_DIR, "**", "*.h"))
|
|
229
|
+
.reject { |f| f.start_with?(File.join(EXT_DIR, "fuzz") + File::SEPARATOR) }
|
|
230
|
+
.map { |f| "$(srcdir)/#{f.sub("#{EXT_DIR}/", "")}" }
|
|
231
|
+
.sort
|
|
232
|
+
File.open("Makefile", "a") do |mk|
|
|
233
|
+
mk.puts
|
|
234
|
+
mk.puts "# Project-header dependencies appended by extconf.rb (mkmf emits none)."
|
|
235
|
+
mk.puts "$(OBJS): #{project_headers.join(" ")}"
|
|
236
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ext/makiri/fuzz/Makefile - libFuzzer harness build
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# make xml_fuzz # build the XML parser harness
|
|
5
|
+
# make xpath_fuzz # build the XPath compile+eval harness
|
|
6
|
+
# make clean # remove build artifacts
|
|
7
|
+
#
|
|
8
|
+
# Requires: clang with libFuzzer support (usually part of the clang distribution)
|
|
9
|
+
# and the vendored Lexbor static library already built (via `rake compile`).
|
|
10
|
+
|
|
11
|
+
CLANG ?= clang
|
|
12
|
+
CXX := $(if $(filter default,$(origin CXX)),clang++,$(CXX))
|
|
13
|
+
|
|
14
|
+
# Paths relative to ext/makiri/fuzz/
|
|
15
|
+
EXT_DIR = ..
|
|
16
|
+
LEXBOR_SRC = ../../../vendor/lexbor
|
|
17
|
+
LEXBOR_DST = $(LEXBOR_SRC)/dist
|
|
18
|
+
|
|
19
|
+
# CFLAGS mirror the security flags from extconf.rb, minus Ruby-specific flags.
|
|
20
|
+
# Sanitizer instrumentation is added at compile time so every TU is covered.
|
|
21
|
+
SANITIZE ?= address,undefined
|
|
22
|
+
CFLAGS = -O2 -g -Wall -Wextra \
|
|
23
|
+
-fstack-protector-strong \
|
|
24
|
+
-Wformat -Wformat-security \
|
|
25
|
+
-fvisibility=hidden \
|
|
26
|
+
-fno-common \
|
|
27
|
+
-fsanitize=$(SANITIZE) \
|
|
28
|
+
-I$(LEXBOR_DST)/include \
|
|
29
|
+
-I$(EXT_DIR) \
|
|
30
|
+
-I$(EXT_DIR)/core \
|
|
31
|
+
-I$(EXT_DIR)/xml \
|
|
32
|
+
-I$(EXT_DIR)/xpath \
|
|
33
|
+
-I$(EXT_DIR)/lexbor_compat
|
|
34
|
+
|
|
35
|
+
# Linker flags: libFuzzer driver + sanitizers.
|
|
36
|
+
LDFLAGS = -fsanitize=fuzzer,$(SANITIZE)
|
|
37
|
+
|
|
38
|
+
BUILD_DIR = build
|
|
39
|
+
|
|
40
|
+
# Ruby-free C sources that the harnesses link against.
|
|
41
|
+
CORE_SRCS = \
|
|
42
|
+
$(EXT_DIR)/core/mkr_alloc.c \
|
|
43
|
+
$(EXT_DIR)/core/mkr_utf8.c \
|
|
44
|
+
$(EXT_DIR)/core/mkr_buf.c \
|
|
45
|
+
$(EXT_DIR)/core/mkr_core.c
|
|
46
|
+
|
|
47
|
+
XML_SRCS = \
|
|
48
|
+
$(EXT_DIR)/xml/mkr_xml_tree.c \
|
|
49
|
+
$(EXT_DIR)/xml/mkr_xml_node.c \
|
|
50
|
+
$(EXT_DIR)/xml/mkr_xml_chars.c \
|
|
51
|
+
$(EXT_DIR)/xml/mkr_xml_index.c \
|
|
52
|
+
$(EXT_DIR)/xml/mkr_xml_mutate.c
|
|
53
|
+
|
|
54
|
+
XPATH_SRCS = \
|
|
55
|
+
$(EXT_DIR)/xpath/mkr_xpath.c \
|
|
56
|
+
$(EXT_DIR)/xpath/mkr_xpath_lex.c \
|
|
57
|
+
$(EXT_DIR)/xpath/mkr_xpath_parse.c \
|
|
58
|
+
$(EXT_DIR)/xpath/mkr_xpath_shared.c \
|
|
59
|
+
$(EXT_DIR)/xpath/mkr_xpath_number.c \
|
|
60
|
+
$(EXT_DIR)/xpath/mkr_xpath_engine_xml.c \
|
|
61
|
+
$(EXT_DIR)/xpath/mkr_xpath_engine_html.c
|
|
62
|
+
|
|
63
|
+
CORE_OBJS = $(patsubst $(EXT_DIR)/%.c,$(BUILD_DIR)/%.o,$(CORE_SRCS))
|
|
64
|
+
XML_OBJS = $(patsubst $(EXT_DIR)/%.c,$(BUILD_DIR)/%.o,$(XML_SRCS))
|
|
65
|
+
XPATH_OBJS = $(patsubst $(EXT_DIR)/%.c,$(BUILD_DIR)/%.o,$(XPATH_SRCS))
|
|
66
|
+
|
|
67
|
+
.PHONY: all clean check-libfuzzer xml_fuzz xpath_fuzz
|
|
68
|
+
|
|
69
|
+
all: check-libfuzzer xml_fuzz xpath_fuzz
|
|
70
|
+
|
|
71
|
+
check-libfuzzer:
|
|
72
|
+
@mkdir -p $(BUILD_DIR)
|
|
73
|
+
@$(CXX) $(LDFLAGS) -o $(BUILD_DIR)/check_fuzzer check_fuzzer.cc >/dev/null 2>&1 || \
|
|
74
|
+
(echo "libFuzzer runtime not available for $(CXX). Install LLVM clang and run with CLANG=/path/to/clang CXX=/path/to/clang++." >&2; exit 1)
|
|
75
|
+
|
|
76
|
+
xml_fuzz: $(BUILD_DIR)/xml_fuzz.o $(CORE_OBJS) $(XML_OBJS)
|
|
77
|
+
$(CXX) $(LDFLAGS) -o $@ $^
|
|
78
|
+
|
|
79
|
+
xpath_fuzz: $(BUILD_DIR)/xpath_fuzz.o $(CORE_OBJS) $(XML_OBJS) $(XPATH_OBJS)
|
|
80
|
+
$(CXX) $(LDFLAGS) -o $@ $^ $(LEXBOR_DST)/lib/liblexbor_static.a
|
|
81
|
+
|
|
82
|
+
$(BUILD_DIR)/%.o: $(EXT_DIR)/%.c
|
|
83
|
+
@mkdir -p $(dir $@)
|
|
84
|
+
$(CLANG) $(CFLAGS) -c -o $@ $<
|
|
85
|
+
|
|
86
|
+
$(BUILD_DIR)/xml_fuzz.o: xml_fuzz.c
|
|
87
|
+
@mkdir -p $(dir $@)
|
|
88
|
+
$(CLANG) $(CFLAGS) -c -o $@ $<
|
|
89
|
+
|
|
90
|
+
$(BUILD_DIR)/xpath_fuzz.o: xpath_fuzz.c
|
|
91
|
+
@mkdir -p $(dir $@)
|
|
92
|
+
$(CLANG) $(CFLAGS) -c -o $@ $<
|
|
93
|
+
|
|
94
|
+
clean:
|
|
95
|
+
rm -rf $(BUILD_DIR) xml_fuzz xpath_fuzz
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* xml_fuzz.c - libFuzzer harness for mkr_xml_parse.
|
|
2
|
+
*
|
|
3
|
+
* Coverage-guided fuzzing of the XML tokenizer + tree builder.
|
|
4
|
+
* Ruby-free; runs directly on the pure-C parser surface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#include <stdint.h>
|
|
8
|
+
#include <stddef.h>
|
|
9
|
+
#include "xml/mkr_xml.h"
|
|
10
|
+
|
|
11
|
+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
|
12
|
+
{
|
|
13
|
+
mkr_xml_status_t status;
|
|
14
|
+
/* The parser contract says "valid UTF-8, NUL-free", but the fuzzer feeds
|
|
15
|
+
* raw bytes. mkr_xml_parse is fail-closed: it validates as it goes and
|
|
16
|
+
* returns an error status on malformed input, never a partial document.
|
|
17
|
+
* We pass the bytes through untouched so the fuzzer can reach the invalid-
|
|
18
|
+
* UTF-8 / unexpected-NUL error paths too. */
|
|
19
|
+
mkr_xml_doc_t *doc = mkr_xml_parse((const char *) data, size, &status);
|
|
20
|
+
if (doc) {
|
|
21
|
+
mkr_xml_doc_destroy(doc);
|
|
22
|
+
}
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* xpath_fuzz.c - libFuzzer harness for the XPath compile + eval path.
|
|
2
|
+
*
|
|
3
|
+
* Coverage-guided fuzzing of the XPath lexer, parser, and evaluator.
|
|
4
|
+
* We build a small fixed XML document as the evaluation context, then
|
|
5
|
+
* treat the fuzzer input as the XPath expression string.
|
|
6
|
+
*
|
|
7
|
+
* Ruby-free; runs directly on the pure-C engine surface.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include <stdint.h>
|
|
11
|
+
#include <stddef.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
#include "core/mkr_alloc.h"
|
|
14
|
+
#include "xml/mkr_xml.h"
|
|
15
|
+
#include "xpath/mkr_xpath.h"
|
|
16
|
+
#include "xpath/mkr_xpath_internal.h"
|
|
17
|
+
|
|
18
|
+
/* A small, fixed XML document that gives the evaluator something to walk.
|
|
19
|
+
* The expression is the fuzzer input; the document is static so the coverage
|
|
20
|
+
* signal comes from the engine, not the parser. */
|
|
21
|
+
static const char FIXED_XML[] =
|
|
22
|
+
"<?xml version='1.0'?>"
|
|
23
|
+
"<root xmlns='http://example.com/default' xmlns:ns='http://example.com/ns'>"
|
|
24
|
+
" <a id='1' ns:attr='x'>text1</a>"
|
|
25
|
+
" <b id='2'><c/><c/></b>"
|
|
26
|
+
" <ns:d>namespaced</ns:d>"
|
|
27
|
+
" <!-- comment -->"
|
|
28
|
+
" <?pi target='value'?>"
|
|
29
|
+
"</root>";
|
|
30
|
+
|
|
31
|
+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
|
32
|
+
{
|
|
33
|
+
/* 1. Parse the fixed document. */
|
|
34
|
+
mkr_xml_status_t status;
|
|
35
|
+
mkr_xml_doc_t *doc = mkr_xml_parse(FIXED_XML, sizeof(FIXED_XML) - 1, &status);
|
|
36
|
+
if (!doc) return 0;
|
|
37
|
+
if (!doc->doc_node) {
|
|
38
|
+
mkr_xml_doc_destroy(doc);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* 2. The fuzzer input is the XPath expression.
|
|
43
|
+
* The engine text contract requires no interior NUL and a NUL at
|
|
44
|
+
* ptr[len]. libFuzzer hands us exactly `size` bytes with no terminator,
|
|
45
|
+
* so we copy the expression prefix into an owned, NUL-terminated heap
|
|
46
|
+
* buffer and mint the verified-text token over that copy - this is what
|
|
47
|
+
* supplies the NUL-termination + no-interior-NUL the lexer's strtod and
|
|
48
|
+
* "%.10s" error path rely on. If the input contains a NUL we truncate to
|
|
49
|
+
* the prefix (the lexer hits the terminator and reports a syntax error,
|
|
50
|
+
* a path worth exercising). UTF-8 validity is deliberately NOT
|
|
51
|
+
* pre-checked: the lexer's strict decoder rejecting invalid UTF-8 is
|
|
52
|
+
* itself a path the fuzzer should hit. */
|
|
53
|
+
size_t expr_len = size;
|
|
54
|
+
for (size_t i = 0; i < size; i++) {
|
|
55
|
+
if (data[i] == '\0') {
|
|
56
|
+
expr_len = i;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
char *expr_copy = mkr_strndup((const char *) data, expr_len);
|
|
61
|
+
if (!expr_copy) {
|
|
62
|
+
mkr_xml_doc_destroy(doc);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
/* Empty expression is a quick syntax error; still worth a run. */
|
|
66
|
+
mkr_verified_text_t expr = { expr_copy, expr_len };
|
|
67
|
+
|
|
68
|
+
/* 3. Compile the expression. */
|
|
69
|
+
mkr_xpath_limits_t limits;
|
|
70
|
+
mkr_xpath_limits_init_defaults(&limits);
|
|
71
|
+
/* Tighten the compile-time budgets so a hostile expression fails fast
|
|
72
|
+
* rather than burning fuzzer time on pathological ASTs. */
|
|
73
|
+
limits.max_ast_nodes = 10000;
|
|
74
|
+
limits.max_expr_bytes = 16 * 1024;
|
|
75
|
+
|
|
76
|
+
mkr_xpath_error_t err = {0};
|
|
77
|
+
mkr_node_t *ast = mkr_parse(expr, &limits, &err);
|
|
78
|
+
if (!ast) {
|
|
79
|
+
mkr_xpath_error_clear(&err);
|
|
80
|
+
free(expr_copy);
|
|
81
|
+
mkr_xml_doc_destroy(doc);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* 4. Evaluate against the fixed document. */
|
|
86
|
+
mkr_xpath_context_t *ctx = mkr_xpath_context_new(doc->doc_node, doc->doc_node);
|
|
87
|
+
if (ctx) {
|
|
88
|
+
mkr_xpath_set_engine_kind(ctx, 1); /* XML engine */
|
|
89
|
+
mkr_xpath_limits_init_defaults(&limits);
|
|
90
|
+
limits.max_eval_ops = 5 * 1000 * 1000; /* 5M ops - enough for a real query */
|
|
91
|
+
limits.max_nodeset_size = 10000;
|
|
92
|
+
limits.max_string_bytes = 1024 * 1024;
|
|
93
|
+
limits.max_recursion_depth = 64;
|
|
94
|
+
|
|
95
|
+
mkr_xpath_value_t out = {0};
|
|
96
|
+
mkr_xpath_error_t eval_err = {0};
|
|
97
|
+
if (mkr_xpath_eval_compiled(ctx, ast, &out, &eval_err) == 0) {
|
|
98
|
+
mkr_xpath_value_clear(&out);
|
|
99
|
+
} else {
|
|
100
|
+
mkr_xpath_error_clear(&eval_err);
|
|
101
|
+
}
|
|
102
|
+
mkr_xpath_context_free(ctx);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
mkr_node_free(ast);
|
|
106
|
+
free(expr_copy);
|
|
107
|
+
mkr_xml_doc_destroy(doc);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
data/ext/makiri/glue/glue.h
CHANGED
|
@@ -64,6 +64,14 @@ lxb_dom_node_t *mkr_html_node_unwrap(VALUE rb_node);
|
|
|
64
64
|
void *mkr_node_raw(VALUE rb_node);
|
|
65
65
|
uintptr_t mkr_node_id(VALUE rb_node);
|
|
66
66
|
|
|
67
|
+
/* Representation-neutral identity methods (ruby_node.c): depend only on
|
|
68
|
+
* mkr_node_id, so the HTML and XML NodeMethods modules bind ==/eql? to
|
|
69
|
+
* mkr_node_equals, hash to mkr_node_hash, and pointer_id to mkr_node_pointer_id -
|
|
70
|
+
* one implementation, not one per representation. */
|
|
71
|
+
VALUE mkr_node_equals(VALUE self, VALUE other);
|
|
72
|
+
VALUE mkr_node_pointer_id(VALUE self);
|
|
73
|
+
VALUE mkr_node_hash(VALUE self);
|
|
74
|
+
|
|
67
75
|
/* XML node bridge (glue/ruby_xml_node.c): wrap a custom XML node into the right
|
|
68
76
|
* Makiri::XML::* leaf (Qnil for NULL, the Document VALUE for the document node). */
|
|
69
77
|
struct mkr_xml_node;
|
data/ext/makiri/glue/ruby_doc.c
CHANGED
|
@@ -140,10 +140,10 @@ mkr_resolve_fragment_context(lxb_dom_document_t *doc, VALUE context,
|
|
|
140
140
|
mkr_ruby_borrowed_text_t cv = mkr_ruby_verified_text(context, "fragment context element");
|
|
141
141
|
const lxb_char_t *p = (const lxb_char_t *)cv.ptr;
|
|
142
142
|
size_t n = cv.len;
|
|
143
|
-
if (
|
|
143
|
+
if (mkr_bytes_eq(p, n, "svg", 3)) {
|
|
144
144
|
*out_tag = LXB_TAG_SVG; *out_ns = LXB_NS_SVG; return;
|
|
145
145
|
}
|
|
146
|
-
if (
|
|
146
|
+
if (mkr_bytes_eq(p, n, "math", 4)) {
|
|
147
147
|
*out_tag = LXB_TAG_MATH; *out_ns = LXB_NS_MATH; return;
|
|
148
148
|
}
|
|
149
149
|
lxb_tag_id_t tid = lxb_tag_id_by_name(doc->tags, p, n);
|
|
@@ -248,7 +248,6 @@ mkr_sanitize_html_input(VALUE html, const lxb_char_t **out, size_t *out_len,
|
|
|
248
248
|
size_t n = (hv.len > 0) ? hv.len : 1;
|
|
249
249
|
char *buf = mkr_reallocarray(NULL, n, 1);
|
|
250
250
|
if (buf == NULL) {
|
|
251
|
-
RB_GC_GUARD(hv.value);
|
|
252
251
|
return -1;
|
|
253
252
|
}
|
|
254
253
|
if (hv.len > 0) {
|
|
@@ -268,19 +267,16 @@ mkr_sanitize_html_input(VALUE html, const lxb_char_t **out, size_t *out_len,
|
|
|
268
267
|
*owned = NULL;
|
|
269
268
|
*out = (const lxb_char_t *)hv.ptr;
|
|
270
269
|
*out_len = hv.len;
|
|
271
|
-
RB_GC_GUARD(hv.value);
|
|
272
270
|
return 0;
|
|
273
271
|
}
|
|
274
272
|
lxb_char_t *clean = NULL;
|
|
275
273
|
size_t clean_len = 0;
|
|
276
274
|
if (mkr_utf8_sanitize((const lxb_char_t *)hv.ptr, hv.len, &clean, &clean_len) != 0) {
|
|
277
|
-
RB_GC_GUARD(hv.value);
|
|
278
275
|
return -1;
|
|
279
276
|
}
|
|
280
277
|
*owned = clean;
|
|
281
278
|
*out = (clean != NULL) ? clean : (const lxb_char_t *)hv.ptr;
|
|
282
279
|
*out_len = (clean != NULL) ? clean_len : hv.len;
|
|
283
|
-
RB_GC_GUARD(hv.value);
|
|
284
280
|
return 0;
|
|
285
281
|
}
|
|
286
282
|
|
|
@@ -509,28 +505,28 @@ mkr_doc_s_parse(VALUE klass, VALUE rb_source)
|
|
|
509
505
|
* preserved rather than read as raw UTF-8 bytes. */
|
|
510
506
|
rb_source = mkr_ruby_to_utf8(rb_source);
|
|
511
507
|
|
|
512
|
-
/*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
* store. The source is not retained past the parse (Lexbor copies what it
|
|
523
|
-
* needs into the arena and the line table is built up front), so the
|
|
524
|
-
* buffer is freed immediately after. */
|
|
508
|
+
/* Copy the source into a C buffer up front - BEFORE allocating the wrapper
|
|
509
|
+
* (a Ruby allocation, and thus a GC point) - so no GC can run between
|
|
510
|
+
* obtaining rb_source (possibly a fresh transcoded String) and copying its
|
|
511
|
+
* bytes, and the parse can then run with the GVL released without racing
|
|
512
|
+
* GC/compaction on the Ruby String's backing store. The source is not
|
|
513
|
+
* retained past the parse (Lexbor copies what it needs into the arena and
|
|
514
|
+
* the line table is built up front), so the buffer is freed immediately
|
|
515
|
+
* after. The coderange is read first (no scan): a source Ruby already knows
|
|
516
|
+
* is valid UTF-8 lets the parse skip its sanitisation scan. */
|
|
517
|
+
bool assume_valid = mkr_ruby_str_known_valid_utf8(rb_source);
|
|
525
518
|
mkr_owned_bytes_t source = {0};
|
|
526
519
|
if (mkr_ruby_copy_bytes(rb_source, &source) != 0) {
|
|
527
520
|
rb_raise(mkr_eError, "out of memory copying source");
|
|
528
521
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
*
|
|
532
|
-
|
|
533
|
-
|
|
522
|
+
|
|
523
|
+
/* Allocate the wrapper (with parsed == NULL) so that if parsing fails the
|
|
524
|
+
* GC-managed object frees cleanly. This is the HTML parse entry (defined on
|
|
525
|
+
* Makiri::HTML::Document), so the result is always HTML. */
|
|
526
|
+
mkr_doc_data_t *d;
|
|
527
|
+
VALUE obj = TypedData_Make_Struct(klass, mkr_doc_data_t, &mkr_html_doc_type, d);
|
|
528
|
+
d->parsed = NULL;
|
|
529
|
+
d->errors = rb_ary_new();
|
|
534
530
|
|
|
535
531
|
mkr_parse_nogvl_t args = { (const lxb_char_t *)source.ptr, source.len,
|
|
536
532
|
assume_valid, NULL };
|
|
@@ -71,11 +71,20 @@ mkr_css_engine_init(void)
|
|
|
71
71
|
g_css_ready = 1;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/* find: the callback runs inside Lexbor's traversal frames, so it must NOT
|
|
75
|
+
* raise - a longjmp would abort lxb_selectors_find mid-walk AND skip the
|
|
76
|
+
* post-run reset in mkr_with_compiled_selector, leaving the process-global
|
|
77
|
+
* parser/memory/selectors dirty for every later query. So matches are collected
|
|
78
|
+
* into a plain C array here (no Ruby at all); the node cap and a growth failure
|
|
79
|
+
* each latch a flag and STOP, and the caller raises only after the traversal
|
|
80
|
+
* has unwound normally and the engine was reset. */
|
|
74
81
|
typedef struct {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
size_t
|
|
78
|
-
|
|
82
|
+
lxb_dom_node_t **nodes; /* malloc-backed; the caller owns + frees it */
|
|
83
|
+
size_t count;
|
|
84
|
+
size_t cap;
|
|
85
|
+
lxb_dom_node_t *root; /* excluded from results: css is descendant-only */
|
|
86
|
+
int overflow;
|
|
87
|
+
int oom;
|
|
79
88
|
} mkr_css_ctx_t;
|
|
80
89
|
|
|
81
90
|
static lxb_status_t
|
|
@@ -92,12 +101,39 @@ mkr_css_find_cb(lxb_dom_node_t *node, lxb_css_selector_specificity_t spec,
|
|
|
92
101
|
c->overflow = 1;
|
|
93
102
|
return LXB_STATUS_STOP; /* fail closed without raising mid-traversal */
|
|
94
103
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
if (mkr_grow_reserve((void **)&c->nodes, &c->cap, c->count + 1,
|
|
105
|
+
sizeof(*c->nodes)) != MKR_OK) {
|
|
106
|
+
c->oom = 1;
|
|
107
|
+
return LXB_STATUS_STOP; /* ditto: report OOM after the walk unwinds */
|
|
108
|
+
}
|
|
109
|
+
c->nodes[c->count++] = node;
|
|
98
110
|
return LXB_STATUS_OK;
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
/* Move the collected matches into the NodeSet under rb_ensure, so the C array
|
|
114
|
+
* cannot leak even if a push raises (NoMemoryError). */
|
|
115
|
+
typedef struct {
|
|
116
|
+
VALUE set;
|
|
117
|
+
mkr_css_ctx_t *ctx;
|
|
118
|
+
} mkr_css_fill_t;
|
|
119
|
+
|
|
120
|
+
static VALUE
|
|
121
|
+
mkr_css_fill_set(VALUE p)
|
|
122
|
+
{
|
|
123
|
+
mkr_css_fill_t *f = (mkr_css_fill_t *)p;
|
|
124
|
+
for (size_t i = 0; i < f->ctx->count; i++) {
|
|
125
|
+
mkr_node_set_push(f->set, (mkr_raw_node_t *)f->ctx->nodes[i]);
|
|
126
|
+
}
|
|
127
|
+
return Qnil;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static VALUE
|
|
131
|
+
mkr_css_free_nodes(VALUE p)
|
|
132
|
+
{
|
|
133
|
+
free(((mkr_css_ctx_t *)p)->nodes);
|
|
134
|
+
return Qnil;
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
/* at_css: capture the first matching descendant and stop. Avoids materialising a
|
|
102
138
|
* NodeSet (and a Ruby #first dispatch) for the one node the caller wants. */
|
|
103
139
|
typedef struct {
|
|
@@ -199,15 +235,25 @@ mkr_node_css(VALUE self, VALUE rb_selector)
|
|
|
199
235
|
{
|
|
200
236
|
lxb_dom_node_t *root = mkr_html_node_unwrap(self);
|
|
201
237
|
VALUE document = mkr_node_document(self);
|
|
202
|
-
VALUE set = mkr_node_set_new(document);
|
|
203
238
|
|
|
204
|
-
|
|
239
|
+
/* A syntax error raises inside mkr_with_compiled_selector BEFORE the find
|
|
240
|
+
* runs, so ctx.nodes is still NULL there - nothing leaks on that path. */
|
|
241
|
+
mkr_css_ctx_t ctx = { .nodes = NULL, .count = 0, .cap = 0,
|
|
242
|
+
.root = root, .overflow = 0, .oom = 0 };
|
|
205
243
|
mkr_with_compiled_selector(rb_selector, root, mkr_run_find, &ctx);
|
|
206
244
|
|
|
207
|
-
if (ctx.overflow) {
|
|
208
|
-
|
|
209
|
-
|
|
245
|
+
if (ctx.overflow || ctx.oom) {
|
|
246
|
+
free(ctx.nodes);
|
|
247
|
+
if (ctx.overflow) {
|
|
248
|
+
rb_raise(mkr_eError, "CSS result set exceeded the node limit (%u)",
|
|
249
|
+
MKR_NODE_SET_MAX);
|
|
250
|
+
}
|
|
251
|
+
rb_raise(mkr_eError, "out of memory collecting CSS results");
|
|
210
252
|
}
|
|
253
|
+
|
|
254
|
+
VALUE set = mkr_node_set_new(document);
|
|
255
|
+
mkr_css_fill_t fill = { set, &ctx };
|
|
256
|
+
rb_ensure(mkr_css_fill_set, (VALUE)&fill, mkr_css_free_nodes, (VALUE)&ctx);
|
|
211
257
|
return set;
|
|
212
258
|
}
|
|
213
259
|
|
|
@@ -265,8 +265,7 @@ mkr_attr_find_ns(lxb_dom_element_t *el, lxb_ns_id_t ns_id,
|
|
|
265
265
|
const lxb_char_t *q = lxb_dom_attr_qualified_name(at, &qlen);
|
|
266
266
|
(void) lxb_dom_attr_local_name(at, &llen);
|
|
267
267
|
if (q != NULL && qlen >= llen
|
|
268
|
-
&& llen
|
|
269
|
-
&& memcmp(q + (qlen - llen), local, local_len) == 0) {
|
|
268
|
+
&& mkr_bytes_eq(q + (qlen - llen), llen, local, local_len)) {
|
|
270
269
|
return at;
|
|
271
270
|
}
|
|
272
271
|
}
|
|
@@ -312,9 +311,11 @@ mkr_node_set_attribute_ns(VALUE self, VALUE rb_ns, VALUE rb_qname, VALUE rb_valu
|
|
|
312
311
|
}
|
|
313
312
|
|
|
314
313
|
const lxb_char_t *qn = (const lxb_char_t *)qv.ptr;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
mkr_span_t qspan = mkr_span((const char *)qn, qv.len);
|
|
315
|
+
size_t colon_off;
|
|
316
|
+
bool has_colon = mkr_span_find(&qspan, ':', &colon_off);
|
|
317
|
+
const lxb_char_t *local = has_colon ? qn + colon_off + 1 : qn;
|
|
318
|
+
size_t local_len = has_colon ? qv.len - colon_off - 1 : qv.len;
|
|
318
319
|
|
|
319
320
|
/* A match keeps its qualified name (so re-setting with a different prefix
|
|
320
321
|
* leaves the prefix unchanged); only the value updates. A miss appends a new
|
|
@@ -507,7 +508,11 @@ mkr_parse_fragment_into(lxb_dom_node_t *context_el, VALUE rb_html,
|
|
|
507
508
|
|
|
508
509
|
mkr_import_fragment_children(doc, frag, emit, u);
|
|
509
510
|
|
|
510
|
-
/*
|
|
511
|
+
/* lxb_html_parse_fragment built the fragment in a TRANSIENT document that
|
|
512
|
+
* destroying the parser does NOT free (measured: one document leaked per
|
|
513
|
+
* inner_html=/outer_html= call); our imported copies live in `doc`, so the
|
|
514
|
+
* transient document is destroyed explicitly. */
|
|
515
|
+
lxb_html_document_destroy(lxb_html_interface_document(frag->owner_document));
|
|
511
516
|
lxb_html_parser_destroy(parser);
|
|
512
517
|
RB_GC_GUARD(html);
|
|
513
518
|
}
|