Skip to content

Commit 8f64929

Browse files
committed
Test that a coerced TYPE_CONST_STRING argument stays in place
A TYPE_CONST_STRING argument that comes from a to_str object is coerced inside Fiddle, so the caller's argv holds that object and not the String that the char * points into. Only converted_args holds the coerced String, and the conservative scan of the ALLOCV buffer is what keeps it in place while ffi_call runs without the GVL. Nothing tested that. Before #205, converted_args was a Ruby Array, whose elements the GC may move, and the C function read the slot the String left. #205 replaced the Array with this buffer and corrected the fault as a side effect. A later change could move these values back to storage the GC may relocate, and no test would notice. Add the missing test, and record the requirement next to the buffer.
1 parent 8dc7c05 commit 8f64929

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

‎ext/fiddle/function.c‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,18 @@ function_call(int argc, VALUE argv[], VALUE self)
304304
(func->is_variadic ? sizeof(int) * n_call_args : 0));
305305
args.values = (void **)((char *)generic_args +
306306
sizeof(fiddle_generic) * n_call_args);
307-
/* GC-scanned (conservatively) as part of the ALLOCV buffer */
307+
/* GC-scanned (conservatively) as part of the ALLOCV buffer.
308+
*
309+
* The conservative scan both keeps these values alive and holds them in
310+
* place, and both properties are required. generic_args can hold a pointer
311+
* into one of them: TYPE_CONST_STRING stores rb_string_value_cstr(&src),
312+
* and for a to_str object that is the coerced String, which argv does not
313+
* hold. ffi_call then runs without the GVL, where another thread can compact
314+
* the heap.
315+
*
316+
* Storing these values somewhere the GC may move them instead, such as a
317+
* Ruby Array, keeps them alive but not in place. The C function then reads
318+
* the slot the String left. Keep them in this buffer. */
308319
converted_args = (VALUE *)((char *)args.values +
309320
sizeof(void *) * (n_call_args + 1));
310321
MEMZERO(converted_args, VALUE, n_call_args);

‎test/fiddle/test_function.rb‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,86 @@ def test_strcpy
179179
assert_equal("123", str.to_s)
180180
end
181181

182+
# A TYPE_CONST_STRING argument that comes from a to_str object is coerced inside
183+
# Fiddle. The caller's argv then holds that object, not the String that the
184+
# char * points into, so argv does not keep the String in place. Fiddle keeps the
185+
# coerced String in converted_args instead, and the conservative scan of the
186+
# ALLOCV buffer pins it there.
187+
#
188+
# That pin is what this test protects. A converted_args that the garbage
189+
# collector can move, such as a Ruby Array, lets compaction move the String after
190+
# the char * is stored. The C function then reads the slot the String left.
191+
#
192+
# The second argument must be neither a String nor a Pointer. Only then does
193+
# Fiddle call Pointer.[] on it, and that allocation is what lets the garbage
194+
# collector run while the char * is already in place.
195+
def test_call_const_string_from_to_str_after_compaction
196+
omit("Need CRuby") unless RUBY_ENGINE == "ruby"
197+
omit("Need GC.auto_compact=") unless GC.respond_to?(:auto_compact=)
198+
199+
length = 100
200+
subject_class = Class.new do
201+
define_method(:to_str) { "Q" * length }
202+
end
203+
# 4096 bytes keep these bytes in a malloc'd buffer, which compaction does not
204+
# move. A short String would hold its bytes in the object itself, and then this
205+
# pointer could dangle and report a failure that this test is not about.
206+
reject = "Z" + ("\0" * 4095)
207+
# to_ptr must build a new Pointer on every call. A Pointer that already exists
208+
# gives Fiddle nothing to allocate, the garbage collector then does not run
209+
# while the char * is in place, and this test cannot fail.
210+
opener_class = Class.new do
211+
define_method(:to_ptr) { Pointer[reject] }
212+
end
213+
214+
strcspn = Function.new(@libc["strcspn"],
215+
[TYPE_CONST_STRING, TYPE_VOIDP],
216+
TYPE_SIZE_T)
217+
# The subject holds no "Z", so strcspn stops at the terminator and reports the
218+
# full length. A stale char * reports 0, because the slot the String left holds
219+
# zero bytes, and churn content also reports 0.
220+
221+
churn = []
222+
results = []
223+
auto_compact = GC.auto_compact
224+
begin
225+
10.times do
226+
# Churn of the same byte size as the subject, with half of it dropped, so
227+
# that the subject's size pool holds sparsely occupied pages. The compactor
228+
# only evacuates such pages. Without this the subject never moves, and then
229+
# this test cannot fail.
230+
churn.clear
231+
2000.times { churn << ("Z" * length) }
232+
churn.each_index { |i| churn[i] = nil if i.even? }
233+
GC.start
234+
235+
# A short burst of same size allocations that this loop drops at once.
236+
# It leaves a partly filled page in the subject's size pool. The
237+
# compactor only moves objects out of pages that are not full, so
238+
# without this burst the subject can land in a full page. It then never
239+
# moves, and this test cannot fail.
240+
500.times { "y" * length }
241+
242+
GC.auto_compact = true
243+
GC.stress = true
244+
begin
245+
results << strcspn.call(subject_class.new, opener_class.new)
246+
ensure
247+
GC.stress = false
248+
GC.auto_compact = auto_compact
249+
end
250+
end
251+
ensure
252+
GC.stress = false
253+
GC.auto_compact = auto_compact
254+
end
255+
assert_equal([length] * 10, results)
256+
# The intact case, checked after the loop: a call before it would prepare the
257+
# CIF, and that preparation is part of what lets the collector run inside the
258+
# window on the first measured call.
259+
assert_equal(length, strcspn.call("Q" * length, opener_class.new))
260+
end
261+
182262
def call_proc(string_to_copy)
183263
buff = +"000"
184264
str = yield(buff, string_to_copy)

0 commit comments

Comments
 (0)