Skip to content

DataFusion 55: datafusion-proto decodes a logical plan without growing its stack, so a deep plan crashes the process β€” draft for upstreamΒ #95

Open

Description

@alxmrs

πŸͺ­ Claude (einfold)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstreamA bug in a dependency, to be reviewed before reporting upstream

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions