From ae58ad403d11f5d31c84ed9513bec650efaeed14 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 05:37:50 -0700 Subject: [PATCH 1/4] Fix wavelet coefficient buffer under allocation in five bindings perform_wavelet_transform writes wt->outlength doubles into the caller supplied buffer. The safe upper bound taken from the wavelib sources is data_len + 2 * decomposition_level * (40 + 1), which is what the C++ binding, the java binding, the rust binding, the swift binding and the internal caller in data_handler.cpp all use. The python, C#, node, matlab and julia bindings dropped the decomposition_level factor and allocated data_len + 2 * (40 + 1). With longer wavelet filters the transform then writes past the end of the buffer. Measured with db15, a 1024 sample input and level 5 writes 1167 doubles into a 1106 slot buffer, 488 bytes past the end. A guard page placed immediately after the buffer turns that into a hard fault. Because each wrapper then slices the buffer to sum(lengths), which is larger than the buffer itself, the coefficient vector handed back to the caller was also silently truncated, and the inverse transform read past the end of that short array on the way back. Restore the decomposition_level factor in all five. The C# overload that takes a 2d array also sized itself from data.Length, the total element count, rather than the row length it actually passes as data_len, so use GetLength (1) there to match. --- csharp_package/brainflow/brainflow/data_filter.cs | 4 ++-- julia_package/brainflow/src/data_filter.jl | 2 +- matlab_package/brainflow/DataFilter.m | 2 +- nodejs_package/brainflow/data_filter.ts | 2 +- python_package/brainflow/data_filter.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/csharp_package/brainflow/brainflow/data_filter.cs b/csharp_package/brainflow/brainflow/data_filter.cs index b8f14f429..fcd312899 100644 --- a/csharp_package/brainflow/brainflow/data_filter.cs +++ b/csharp_package/brainflow/brainflow/data_filter.cs @@ -351,7 +351,7 @@ public static double get_heart_rate (double[] ppg_ir, double[] ppg_red, int samp /// tuple of wavelet coeffs in format [A(J) D(J) D(J-1) ..... D(1)] where J is decomposition level, A - app coeffs, D - detailed coeffs, and array with lengths for each block public static Tuple perform_wavelet_transform (double[] data, int wavelet, int decomposition_level, int extension) { - double[] wavelet_coeffs = new double[data.Length + 2 * (40 + 1)]; + double[] wavelet_coeffs = new double[data.Length + 2 * decomposition_level * (40 + 1)]; int[] lengths = new int[decomposition_level + 1]; int res = DataHandlerLibrary.perform_wavelet_transform (data, data.Length, wavelet, decomposition_level, extension, wavelet_coeffs, lengths); if (res != (int)BrainFlowExitCodes.STATUS_OK) @@ -1115,7 +1115,7 @@ public static unsafe double get_railed_percentage (double[,] data, int row_num, /// tuple of wavelet coeffs in format [A(J) D(J) D(J-1) ..... D(1)] where J is decomposition level, A - app coeffs, D - detailed coeffs, and array with lengths for each block public static unsafe Tuple perform_wavelet_transform (double[,] data, int row_num, int wavelet, int decomposition_level, int extension) { - double[] wavelet_coeffs = new double[data.Length + 2 * (40 + 1)]; + double[] wavelet_coeffs = new double[data.GetLength (1) + 2 * decomposition_level * (40 + 1)]; int[] lengths = new int[decomposition_level + 1]; int res = (int)BrainFlowExitCodes.STATUS_OK; if ((row_num < 0) || (row_num >= data.GetLength (0))) diff --git a/julia_package/brainflow/src/data_filter.jl b/julia_package/brainflow/src/data_filter.jl index 3c5e6ad42..70b7efa00 100644 --- a/julia_package/brainflow/src/data_filter.jl +++ b/julia_package/brainflow/src/data_filter.jl @@ -297,7 +297,7 @@ end end @brainflow_rethrow function perform_wavelet_transform(data, wavelet::WaveletType, decomposition_level::Integer, extension::WaveletExtensionType) - wavelet_coeffs = Vector{Float64}(undef, length(data) + 2 * (40 + 1)) + wavelet_coeffs = Vector{Float64}(undef, length(data) + 2 * decomposition_level * (40 + 1)) lengths = Vector{Cint}(undef, decomposition_level + 1) ccall((:perform_wavelet_transform, DATA_HANDLER_INTERFACE), Cint, (Ptr{Float64}, Cint, Cint, Cint, Cint, Ptr{Float64}, Ptr{Cint}), data, length(data), Int32(wavelet), Int32(decomposition_level), Int32(extension), wavelet_coeffs, lengths) diff --git a/matlab_package/brainflow/DataFilter.m b/matlab_package/brainflow/DataFilter.m index e5764ed1b..aefa98f39 100644 --- a/matlab_package/brainflow/DataFilter.m +++ b/matlab_package/brainflow/DataFilter.m @@ -186,7 +186,7 @@ function disable_data_logger() task_name = 'perform_wavelet_transform'; temp_input = libpointer('doublePtr', data); lib_name = DataFilter.load_lib(); - temp_output = libpointer('doublePtr', zeros(1, int32(size(data, 2) + 2 *(40 + 1)))); + temp_output = libpointer('doublePtr', zeros(1, int32(size(data, 2) + 2 * decomposition_level * (40 + 1)))); lenghts = libpointer('int32Ptr', zeros(1, decomposition_level + 1)); exit_code = calllib(lib_name, task_name, temp_input, size(data, 2), wavelet, decomposition_level, extension, temp_output, lenghts); DataFilter.check_ec(exit_code, task_name); diff --git a/nodejs_package/brainflow/data_filter.ts b/nodejs_package/brainflow/data_filter.ts index 66ea6a94d..b032298f4 100644 --- a/nodejs_package/brainflow/data_filter.ts +++ b/nodejs_package/brainflow/data_filter.ts @@ -491,7 +491,7 @@ export class DataFilter public static performWaveletTransform(data: number[], wavelet: WaveletTypes, decompositionLevel: number, extension: WaveletExtensionTypes): [number[], number[]] { - const waveletCoeffs = [...new Array (data.length + 2 * (40 + 1)).fill(0)]; + const waveletCoeffs = [...new Array (data.length + 2 * decompositionLevel * (40 + 1)).fill(0)]; const lengths = [...new Array (decompositionLevel + 1).fill(0)]; const res = DataHandlerDLL.getInstance().performWaveletTransform( data, data.length, wavelet, decompositionLevel, extension, waveletCoeffs, lengths); diff --git a/python_package/brainflow/data_filter.py b/python_package/brainflow/data_filter.py index d98a3dda2..3ea170406 100644 --- a/python_package/brainflow/data_filter.py +++ b/python_package/brainflow/data_filter.py @@ -878,7 +878,7 @@ def perform_wavelet_transform(cls, data, wavelet: int, decomposition_level: int, """ check_memory_layout_row_major(data, 1) - wavelet_coeffs = numpy.zeros(data.shape[0] + 2 * (40 + 1)).astype(numpy.float64) + wavelet_coeffs = numpy.zeros(data.shape[0] + 2 * decomposition_level * (40 + 1)).astype(numpy.float64) lengths = numpy.zeros(decomposition_level + 1).astype(numpy.int32) res = DataHandlerDLL.get_instance().perform_wavelet_transform(data, data.shape[0], wavelet, decomposition_level, extension_type, From 1eb401e3e216fe0a7b9bbe50a30cd86034855fae Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 20:36:45 -0700 Subject: [PATCH 2/4] Add wavelet coefficient buffer size regression test Asserts the returned coefficient count equals sum(lengths) for db15 at every level up to 5, and that the inverse transform round-trips. The old fixed headroom allocated 1106 doubles where the native side writes 1167. --- .../examples/tests/wavelet_buffer_size.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 python_package/examples/tests/wavelet_buffer_size.py diff --git a/python_package/examples/tests/wavelet_buffer_size.py b/python_package/examples/tests/wavelet_buffer_size.py new file mode 100644 index 000000000..3891a5095 --- /dev/null +++ b/python_package/examples/tests/wavelet_buffer_size.py @@ -0,0 +1,51 @@ +import numpy as np + +from brainflow.data_filter import DataFilter, WaveletExtensionTypes, WaveletTypes + +# db15 has a 30 tap filter, so a level 5 decomposition needs a reasonably long signal and +# grows the coefficient array well past the old fixed 2 * (40 + 1) headroom. +DATA_LEN = 1024 +DECOMPOSITION_LEVEL = 5 + + +def main(): + rng = np.random.default_rng(3) + data = rng.standard_normal(DATA_LEN) + + output = DataFilter.perform_wavelet_transform( + np.copy(data), WaveletTypes.DB15, DECOMPOSITION_LEVEL, WaveletExtensionTypes.SYMMETRIC + ) + coeffs, lengths = output + + # The binding allocated data_len + 2 * (40 + 1) regardless of decomposition level, but + # the native side writes sum(lengths) doubles. At db15 level 5 that is 1167 against an + # allocation of 1106, so the transform wrote past the end of the array. Comparing the + # returned coefficient count against sum(lengths) is what makes the shortfall visible, + # because the wrapper slices the buffer it allocated. + assert coeffs.shape[0] == int(np.sum(lengths)), (coeffs.shape[0], int(np.sum(lengths))) + + # The headroom has to scale with the level, so an under-allocation at any level shows up. + for level in range(1, DECOMPOSITION_LEVEL + 1): + level_output = DataFilter.perform_wavelet_transform( + np.copy(data), WaveletTypes.DB15, level, WaveletExtensionTypes.SYMMETRIC + ) + level_coeffs, level_lengths = level_output + assert level_coeffs.shape[0] == int(np.sum(level_lengths)), ( + level, + level_coeffs.shape[0], + int(np.sum(level_lengths)), + ) + + # A truncated coefficient array cannot reconstruct the signal, so the round trip is the + # end-to-end check that nothing was lost. + restored = DataFilter.perform_inverse_wavelet_transform( + output, DATA_LEN, WaveletTypes.DB15, DECOMPOSITION_LEVEL, WaveletExtensionTypes.SYMMETRIC + ) + assert restored.shape[0] == DATA_LEN, restored.shape + assert np.allclose(restored, data, atol=1e-8), np.max(np.abs(restored - data)) + + print('wavelet buffer size regression passed') + + +if __name__ == '__main__': + main() From 81e47d21c8adedb368ea77439576a1eeab520b04 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 20:37:29 -0700 Subject: [PATCH 3/4] Run the new regression test in unix CI --- .github/workflows/run_unix.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 557001dae..8a9c6b201 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -354,6 +354,8 @@ jobs: run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/transforms.py - name: Downsampling Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/downsampling.py + - name: Wavelet Buffer Size Python + run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/wavelet_buffer_size.py - name: ICA Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/ica.py - name: CSP Python From 9106eac6f9bf1d02a08f3a0015ad34b23a0f4290 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 21:17:11 -0700 Subject: [PATCH 4/4] Run the new regression test in windows CI too --- .github/workflows/run_windows.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run_windows.yml b/.github/workflows/run_windows.yml index 244c3f0e5..537773653 100644 --- a/.github/workflows/run_windows.yml +++ b/.github/workflows/run_windows.yml @@ -235,6 +235,9 @@ jobs: - name: Downsampling Python Test run: python %GITHUB_WORKSPACE%\python_package\examples\tests\downsampling.py shell: cmd + - name: Wavelet Buffer Size Python Test + run: python %GITHUB_WORKSPACE%\python_package\examples\tests\wavelet_buffer_size.py + shell: cmd - name: CSP Python Test run: python %GITHUB_WORKSPACE%\python_package\examples\tests\csp.py shell: cmd