Skip to content

Fix out-of-bounds write for causal ancestral alleles - #192

Merged
jeromekelleher merged 4 commits into
tskit-dev:mainfrom
jeromekelleher:fix-bounds-error
Sep 2, 2026
Merged

Fix out-of-bounds write for causal ancestral alleles#192
jeromekelleher merged 4 commits into
tskit-dev:mainfrom
jeromekelleher:fix-bounds-error

Conversation

@jeromekelleher

Copy link
Copy Markdown
Member

The node traversal allocated an array with one entry per node, but pushes the virtual root when the causal allele is the ancestral allele. The virtual root's ID is num_nodes, so the traversal wrote one element past the end of the array. Numba compiles with bounds checking disabled, so this failed silently.

Move the compiled kernels into a new tstrait.jit module, sizing the traversal buffer from the tree arrays, which already include the virtual root, and dropping it from the returned values. The stack is now a preallocated array with the causal nodes passed in as an array, which removes numba.typed from the codebase along with the special case for an empty stack.

Add tests/test_jit.py, which exercises the kernels through their py_func attribute. Running the untranslated Python gives us numpy's bounds checking and lets coverage measure the kernels, which reach 100% statement and branch coverage from small hand written examples.

The node traversal allocated an array with one entry per node, but pushes
the virtual root when the causal allele is the ancestral allele. The
virtual root's ID is num_nodes, so the traversal wrote one element past
the end of the array. Numba compiles with bounds checking disabled, so
this failed silently.

Move the compiled kernels into a new tstrait.jit module, sizing the
traversal buffer from the tree arrays, which already include the virtual
root, and dropping it from the returned values. The stack is now a
preallocated array with the causal nodes passed in as an array, which
removes numba.typed from the codebase along with the special case for an
empty stack.

Add tests/test_jit.py, which exercises the kernels through their py_func
attribute. Running the untranslated Python gives us numpy's bounds
checking and lets coverage measure the kernels, which reach 100%
statement and branch coverage from small hand written examples.
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4803791) to head (2ef7834).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #192   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            8         9    +1     
  Lines          394       417   +23     
  Branches        54        56    +2     
=========================================
+ Hits           394       417   +23     
Flag Coverage Δ
python-tests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gregorgorjanc

Copy link
Copy Markdown
Member

Looks ok to me, from what I understand is going on here! Will drop linking to the issue: #191

Replace the single hand written tree with trees from the tskit
generators, covering polytomies at and below the root, ladders, and the
degenerate cases in which the virtual root has no children, one child,
or many: a single node, a tree sequence with no nodes, isolated samples,
and a forest containing an unreachable node. Each topology is drawn in
the class docstring so that the expected values can be checked against
it.

Run every test against both the compiled kernel and the Python it was
written as, through the jit and nojit parameters of the fixtures. This
replaces the smoke test comparing the two implementations to each other,
since they are now both checked against fixed expectations.

Use the documented tree sequences in tests/data.py for the accumulation
kernel, rather than inventing node to individual maps, and add tests
composing the two kernels the way genetic_value does.
@jeromekelleher
jeromekelleher merged commit c1d5dba into tskit-dev:main Sep 2, 2026
11 checks passed
@jeromekelleher
jeromekelleher deleted the fix-bounds-error branch September 2, 2026 12:33
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.

2 participants