You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
DataFusion 55: datafusion-proto decodes a logical plan without growing its stack, so a deep plan crashes the process β draft for upstreamΒ #95
DataFusion 55: datafusion-proto decodes a logical plan without growing its stack, so a deep plan crashes the process β draft for upstream.
datafusion-proto is DataFusion's protobuf serialization of plans. DataFusion's FFI, its stable C interface between separately built libraries, uses it to hand a query planner each logical plan, so every plan a datafusion-python session gives einfold is encoded by the session and decoded in einfold's library.
Decoding a plan, AsLogicalPlan::try_into_logical_plan for LogicalPlanNode (datafusion-proto 55.2.0, src/logical_plan/mod.rs, line 499), recurses once per node, with frames of about 8 KB in a release build. Its counterpart for encoding, try_from_logical_plan (line 1310), carries #[cfg_attr(feature = "recursive_protection", recursive::recursive)]. That attribute uses the recursive crate, as DataFusion's other walks of a plan do, to grow the stack as the walk recurses. Decoding has no such attribute. So on a thread with an 8 MB stack, a chain of about 250 joins overflows the stack, and the process dies with SIGSEGV, where DataFusion plans about 1,350.
A second limit sits in front of it: prost's decoder stops at 100 nested messages unless its no-recursion-limit feature is on. A chain of 15 joins is deeper (#77).
Reproduce
With datafusion-python 55.0.0 and einfold's wheel from #75, Linux x86-64: deep.py from #77, a chain of depth joins, planned on a thread with an 8 MB stack. einfold lifts prost's limit, and raises the main thread's stack limit (#75, 832e0b5), so another thread is where this shows.
$ python deep.py 300 # main thread, einfold on: answers
$ (thread with threading.stack_size(8 << 20)) einfold off: answers; einfold on: SIGSEGV
The backtrace at the crash (gdb, 300 joins) is 1,021 frames: about 1,000 of <LogicalPlanNode as AsLogicalPlan>::try_into_logical_plan, under datafusion_proto::bytes::logical_plan_from_bytes_with_extension_codec, under datafusion_ffi::query_planner::create_physical_plan_fn_wrapper's future.
Pinned here
test_decoding_a_deep_plan_overflows_another_threads_stack in python/tests/test_einfold.py (#75) expects that crash, in a subprocess. It fails once decoding grows its stack, and then einfold's workaround (raising the stack limit when it is installed, python/src/stack.rs) can go.
What would fix it
The same attribute on try_into_logical_plan as on try_from_logical_plan, and perhaps on the physical plan's decoding. Then also turning recursive_protection on in datafusion-ffi's dependency on datafusion-proto, since the FFI's planner calls it. A maintainer decides whether to report it upstream.
Activity
Add einfold for Python: an extension for datafusion-python sessions (#75