Skip to content

fix(func): no input, no output - #765

Merged
mario4tier merged 1 commit into
devfrom
fix/empty-input
Sep 14, 2026
Merged

mario4tier merged 1 commit into
devfrom
fix/empty-input

Conversation

@mario4tier

Copy link
Copy Markdown
Member

An empty array given to a Function API wrapper made TA-Lib read and write one element before its buffers. With this change every wrapper returns empty outputs for an empty input, without calling TA-Lib.

The bug

talib.ACOS(np.array([]))   # corrupts the heap; may abort later on an unrelated free, or go unnoticed

check_begidx answers length - 1 for an input with no non-NaN bar, which is -1 for an empty array. The wrapper then calls TA-Lib with startIdx = endIdx = 0 and data + begidx, one element before the buffer, and writes the output at out + lookback. A function whose lookback is zero reads and writes that element.

  • Functions affected: 34 at their default parameters (ACOS, ADD, AD, OBV, BOP, AVGPRICE, the math transforms, CUMSUM, VWAP, WAD, ...), plus any function whose parameters give a zero lookback, such as MA(x, timeperiod=1).
  • APIs affected: the Function API, the pandas and polars wrappers, and abstract.Function.
  • Not affected: stream opens, which raise InsufficientHistory before any call.

When it was introduced

ta-lib-python 0.4.27 (tagged 2023-07-13) introduced it, through commit 02de3a3, "tools/generate_func: don't throw exceptions on all nan input". That release's CHANGELOG reads "[FIX]: Don't throw exceptions when inputs are all NaN".

That commit turned the for/else branch of every check_begidx1..4 from raise Exception("inputs are all NaN") into return length - 1. An empty array takes the same branch, since its loop body never runs. Until 0.4.27 an empty array therefore raised; since then it has reached TA-Lib with begidx = -1.

Every release since has the bug: 0.4.27+, 0.5.x, 0.6.x, 0.7.x and 0.8.0. It is a wrapper bug, independent of the TA-Lib C version.

The fix

No input, no output. tools/generate_func.py emits, right after the length check:

    if length == 0:
        return make_double_array(0, 0)        # one per output, int32 where the output is an integer

It returns before check_begidx, the lookback, the TA-Lib call and the index shift of MAXINDEX/MININDEX/MINMAXINDEX. The generated _func.pxi differs from dev by exactly one such return per function (201). _ta_lib.c is regenerated with Cython 3.3.0.

  • Unchanged for empty input: the argument checks before the return still run. Mismatched lengths, wrong dtypes, 2-D arrays, None, and MAVP with one empty input raise exactly as before.
  • Output types: for all 201 functions, the outputs match the non-empty path in type, dtype and tuple layout, through func, the pandas and polars wrappers, and abstract with dict, pandas DataFrame and polars DataFrame inputs.
  • One behaviour change: an invalid parameter on an empty input now returns empty outputs instead of raising TA_BAD_PARAM, since there is nothing to compute. On a non-empty input it still raises.

Test

tests/test_func.py::test_input_empty runs in a child process with the empty inputs placed right after an inaccessible page (mmap + mprotect). A read before the buffer therefore faults at once instead of corrupting the heap silently. For every function it checks that the empty result has the same number of outputs and dtypes as the result on 100 bars, with length 0. It also covers MA(timeperiod=1) and three invalid parameters.

  • On dev: fails with SIGSEGV (3 of 3).
  • With this change: passes (10 of 10).
  • Wrong-dtype mutant (CDLDOJI returning a float output): fails.
  • Windows: the test is skipped, because the guard needs mprotect and no CI runs the tests there.

Full suite: 1697 passed, built against TA-Lib C 0.8.1 (from the v0.8.1 tag) with Cython 3.3.0 and numpy 2.2.0. Regenerating the .pxi files and _ta_lib.c reproduces the committed ones.

https://claude.ai/code/session_01TuVjx2Q5urdmwi5opzfCp8

An empty array made the Function API call TA-Lib with pointers one element
before its buffers: check_begidx answers length - 1, which is -1, so the
wrapper passed startIdx = endIdx = 0 and data + begidx. A function whose
lookback is zero reads and writes that element. That is 34 functions at
their default parameters, ACOS, ADD and OBV among them, plus any function
whose parameters give a zero lookback, such as MA(x, timeperiod=1), through
the Function, Abstract, pandas and polars paths alike. The heap corruption
may abort the interpreter later, on an unrelated free, or go unnoticed.
Stream opens are not affected; they raise InsufficientHistory first.

When it started: 0.4.27, tagged 2023-07-13. Commit 02de3a3 ("don't throw
exceptions on all nan input", CHANGELOG "[FIX]: Don't throw exceptions when
inputs are all NaN") replaced check_begidx's `raise Exception("inputs are
all NaN")` with `return length - 1`. An empty array takes that same branch,
so until then it raised; since then it has reached TA-Lib with begidx -1.
Every release since has it, through 0.8.0.

Every wrapper now returns its outputs as empty arrays, before check_begidx,
the lookback or TA-Lib are reached. Nothing else changes for an empty input:
types, dtypes and tuple layout match the non-empty path, and for valid
parameters the results are identical to before across func, the pandas and
polars wrappers, and abstract with dict, pandas and polars DataFrame inputs,
for all 201 functions. An invalid parameter on an empty input now returns
empty outputs instead of TA_BAD_PARAM: there is nothing to compute.

The test runs in a child process with the empty inputs right after an
inaccessible page, so a read before the buffer faults at once. It fails on
the previous code with SIGSEGV. It is skipped on Windows, where the guard
would need VirtualProtect and no CI runs the tests.

Claude-Session: https://claude.ai/code/session_01TuVjx2Q5urdmwi5opzfCp8
@mario4tier
mario4tier merged commit 6712c9e into dev Sep 14, 2026
6 checks passed
@mario4tier
mario4tier deleted the fix/empty-input branch September 15, 2026 18:45
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