Skip to content

Commit 2c6b9f4

Browse files
committed
Fix Turso TextValue::free / Blob::free fat-pointer corruption
TextValue stores Box<str>::into_raw() cast to *const u8 (losing the length from the fat pointer). free() reconstructs Box<u8> (size 1 byte) and frees what the allocator tracks as a larger allocation, corrupting the heap. This was the segfault in UDF result freeing (hitting every test that returns text/blob from a PHP UDF, including testFromBase64Function). Rebuild the fat pointer from the stored length at free time.
1 parent 7cbee1f commit 2c6b9f4

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

.github/workflows/phpunit-tests-turso.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,70 @@ jobs:
105105
print(f'patched {n} sqlite3_column_* functions')
106106
PY
107107
108+
# TextValue/Blob `free` reconstructs `Box<u8>` / `Box<u8>` from a
109+
# pointer that was originally `Box<str>` / `Box<[u8]>` (fat
110+
# pointers, length lost in the cast). This corrupts the heap when
111+
# custom UDFs return text/blob values. Fix both frees to use the
112+
# stored length and rebuild the correct slice box.
113+
python3 - <<'PY_FIX_TYPES'
114+
p = '../extensions/core/src/types.rs'
115+
s = open(p).read()
116+
117+
# TextValue::free
118+
old_text = (
119+
" #[cfg(feature = \"core_only\")]\n"
120+
" fn free(self) {\n"
121+
" if !self.text.is_null() {\n"
122+
" let _ = unsafe { Box::from_raw(self.text as *mut u8) };\n"
123+
" }\n"
124+
" }\n"
125+
)
126+
new_text = (
127+
" #[cfg(feature = \"core_only\")]\n"
128+
" fn free(self) {\n"
129+
" if !self.text.is_null() && self.len > 0 {\n"
130+
" unsafe {\n"
131+
" let slice = std::slice::from_raw_parts_mut(\n"
132+
" self.text as *mut u8, self.len as usize);\n"
133+
" let _ = Box::from_raw(slice as *mut [u8]);\n"
134+
" }\n"
135+
" }\n"
136+
" }\n"
137+
)
138+
assert old_text in s, 'TextValue::free not found'
139+
s = s.replace(old_text, new_text, 1)
140+
141+
# Blob::free uses the same pattern. Replace it too if present.
142+
import re
143+
blob_pat = re.compile(
144+
r'(impl Blob \{\n(?:[^}]|\{[^}]*\})*?)'
145+
r'( #\[cfg\(feature = "core_only"\)\]\n'
146+
r' fn free\(self\) \{\n'
147+
r' if !self\.data\.is_null\(\) \{\n'
148+
r' let _ = unsafe \{ Box::from_raw\(self\.data as \*mut u8\) \};\n'
149+
r' \}\n'
150+
r' \}\n)'
151+
)
152+
m = blob_pat.search(s)
153+
if m:
154+
new_blob = (
155+
" #[cfg(feature = \"core_only\")]\n"
156+
" fn free(self) {\n"
157+
" if !self.data.is_null() && self.size > 0 {\n"
158+
" unsafe {\n"
159+
" let slice = std::slice::from_raw_parts_mut(\n"
160+
" self.data as *mut u8, self.size as usize);\n"
161+
" let _ = Box::from_raw(slice as *mut [u8]);\n"
162+
" }\n"
163+
" }\n"
164+
" }\n"
165+
)
166+
s = s[:m.start(2)] + new_blob + s[m.end(2):]
167+
print('patched Blob::free')
168+
open(p, 'w').write(s)
169+
print('patched TextValue::free')
170+
PY_FIX_TYPES
171+
108172
# Turso's create_function_v2 invokes the previous FuncSlot's destroy
109173
# callback when re-registering a UDF with the same name. In practice
110174
# (PHPUnit) this means:

0 commit comments

Comments
 (0)