Skip to content

Fix use-after-free in Connection.read_fls binding (keep_alive) - #71

Open
z0mz0m wants to merge 1 commit into
cwida:devfrom
Zetta-Zip:fix/read-fls-use-after-free
Open

Fix use-after-free in Connection.read_fls binding (keep_alive)#71
z0mz0m wants to merge 1 commit into
cwida:devfrom
Zetta-Zip:fix/read-fls-use-after-free

Conversation

@z0mz0m

@z0mz0m z0mz0m commented Jul 27, 2026

Copy link
Copy Markdown

TableReader stores a reference to its connection:

// src/include/fls/reader/table_reader.hpp
private:
    up<TableDescriptorHandle> m_table_descriptor_handle;
    Connection&               m_connection;   // <-- borrowed
    const path                m_file_path;

but the binding returns it with return_value_policy::move and no lifetime tie, so nothing keeps the Connection alive for as long as the reader. The connection can be collected while the reader is still in use, after which the reader dereferences freed memory.

This crashes the one-liner documented in README.md and used in examples/python_example.py:

pyfastlanes.connect().read_fls('data.fls').to_csv('decoded.csv')

Under lldb on an unfixed build:

EXC_BAD_ACCESS (code=1, address=0x6574617669727077)
frame #0: fastlanes::Rowgroup::Rowgroup(RowgroupDescriptorT const&, Connection const&)

The faulting address is ASCII text — freed std::string bytes being read as a pointer. It reproduces on the bundled data/example corpus, so it is not data-dependent.

py::keep_alive<0, 1>() makes the returned reader keep its Connection alive, which is the invariant the C++ type already assumes.

Verified on macOS 15 / arm64 (Apple clang 21). Three patterns that all crashed before and all pass after, each producing byte-identical output to the pattern that previously worked by accident (holding the connection in a local):

  1. the README chain above (temporary connection);
  2. reader = conn.read_fls(p); del conn; gc.collect(); reader.to_csv(q);
  3. a reader returned from a function whose local connection went out of scope.

Note this branch alone will not compile under clang 21 without #70 (unrelated -Wnonnull failure in TypedStats); the two changes are independent in content but not in build order.

🤖 Generated with Claude Code

TableReader stores 'Connection& m_connection' (table_reader.hpp), but
the binding returned it with return_value_policy::move and no lifetime
tie. The Connection is therefore free to be collected while the reader
is still alive, and the reader then dereferences freed memory.

This crashes the documented one-liner from README.md and
examples/python_example.py:

    pyfastlanes.connect().read_fls('data.fls').to_csv('decoded.csv')

lldb on the unfixed build:

    EXC_BAD_ACCESS (code=1, address=0x6574617669727077)
    frame #0: fastlanes::Rowgroup::Rowgroup(RowgroupDescriptorT const&,
                                            Connection const&)

(the faulting address is ASCII text — freed std::string bytes read as a
pointer). It reproduces on the bundled data/example corpus.

py::keep_alive<0, 1>() makes the returned reader keep its Connection
alive, which is the invariant the C++ type already assumes. Verified:
the README chain, an explicitly deleted connection followed by
gc.collect(), and a reader returned from a function whose local
connection went out of scope all now succeed and produce byte-identical
output to the pattern that previously worked by accident.

Co-Authored-By: Claude Fable 5 <[email protected]>
z0mz0m added a commit to Zetta-Zip/FastLanes that referenced this pull request Jul 27, 2026
…reader

TableReader holds Connection& , but the binding returned it with
return_value_policy::move and no lifetime tie, so the documented
connect().read_fls(p).to_csv(q) chain dereferenced freed memory.
Upstream PR: cwida#71

Co-Authored-By: Claude Fable 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant