CVE-2025-64736

An out-of-bounds read vulnerability exists in the ABF parsing functionality of The Biosig Project libbiosig 3.9.2 and Master Branch (5462afb0). A specially crafted .abf file can lead to an information leak. An attacker can provide a malicious file to trigger this vulnerability.

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

The Biosig Project libbiosig 3.9.2

The Biosig Project libbiosig Master Branch (5462afb0)

libbiosig – https://biosig.sourceforge.net/index.html

6.1 – CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:L

CWE-125 – Out-of-bounds Read

Libbiosig is an open source library designed to process various types of medical signal data (EKG, EEG, etc) within a vast variety of different file formats. Libbiosig is also at the core of biosig APIs in Octave and Matlab, sigviewer, and other scientific software utilized for interpreting biomedical signal data.

Within libbiosig, the `sopen_extended` function is the common entry point for file parsing, regardless of the specific file type:

“`
HDRTYPE* sopen_extended(const char* FileName, const char* MODE, HDRTYPE* hdr, biosig_options_type *biosig_options) { /* MODE=”r” reads file and returns HDR MODE=”w” writes HDR into file */
“`

The general flow of `sopen_extended` is as one might expect: initialize generic structures, determine the relevant file type, parse the file, and finally populate the generic structures that can be utilized by whatever is calling `sopen_extended`. To determine the file type, `sopen_extended` calls `getfiletype`, which attempts to fingerprint the file based on the presence of various magic bytes within the header. Libbiosig also allows for these heuristics to be bypassed by setting the file type manually, but that approach is more applicable when writing data to a file; this vulnerability concerns the code path taken when reading from a file.

The file type used to exercise this vulnerability is the ABF (Axon Binary File) file format, a Molecular Devices propriety format. According to Molecular Device’s user guide, this file format is used for the storage of binary experimental data. To determine if a file is an ABF file, `getfiletype` runs the following check:

“`
else if (!memcmp(hdr->AS.Header, “ABF “, 4)) { // [1] // else if (!memcmp(Header1,”ABF x66x66xE6x3F”,4)) { // ABF v1.8 hdr->TYPE = ABF; hdr->VERSION = lef32p(hdr->AS.Header+4); } else if (!memcmp(hdr->AS.Header, “ABF2x00x00”, 6) && ( hdr->AS.Header[6] < 10 ) && ( hdr->AS.Header[7] < 10 ) ) { hdr->TYPE = ABF2; hdr->VERSION = hdr->AS.Header[7] + ( hdr->AS.Header[6] / 10.0 ); }
“`

Note that `getfiletype` distinguishes between two types of ABF files based on the first few bytes of the header: ABF and ABF2. The original ABF format was superceded by ABF 2.0, but is still supported by libbiosig. This vulnerability is located in the libbiosig code that handles parsing of files using the original ABF format, not 2.0.

Assuming we hit the first branch [1] above, the code flow in `sopen_extended` looks as such:

“`
else if (hdr->TYPE==ABF) { hdr->HeadLen = count; sopen_abf_read(hdr); }
“`

For most file types, the parsing logic is contained within `sopen_extended` itself, with a select few having their own dedicated `sopen_*` function that is called from `sopen_extended`. Such is the case for the ABF file format, which defers to `sopen_abf_read` for the bulk of its parsing logic. For this vulnerability, the relevant code block within `sopen_abf_read` is the following:

“`
hdr->CHANNEL = realloc(hdr->CHANNEL, hdr->NS*sizeof(CHANNEL_TYPE)); // [2] uint32_t k1=0,k; // ABF is 32 bits only, no need for more // [3] for (k=0; k < ABF_ADCCOUNT + ABF_DACCOUNT; k++) { // [4] CHANNEL_TYPE *hc = hdr->CHANNEL+k1; // [8] if ((k < ABF_ADCCOUNT) && (lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k) >= 0)) { // [5] hc->OnOff = 1; hc->bufptr = NULL; hc->LeadIdCode = 0; int ll = min(ABF_ADCNAMELEN, MAX_LENGTH_LABEL); // [7] int16_t nss = lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k); // [9] strncpy(hc->Label, (char*)hdr->AS.Header + offsetof(struct ABFFileHeader, sADCChannelName) + nss*ABF_ADCNAMELEN, ll); // [6]
“`

This block begins by resizing `hdr->CHANNEL`, the heap-allocated array that stores data for each channel in the input file, to accomodate the number of channels defined in that same input file: `hdr-NS` [2]. Next, two counters are initialized, `k` and `k1`, both of which will hold the index of the current channel being processed [3]. The only difference between the two is that `k1` is only used when iterating over the ADC channels, while `k` is used as the index for all channels (both ADC and DAC). From there, the bulk of the processing is done within a for loop that iterates over these channels [4].

This vulnerability concerns the code path taken when the first if statement inside this for loop [5] evaluates to true, which occurs when the current channel being processed ( `k`) is an ADC channel. The vulnerability itself is exercised during a call to `strncpy` [6] that attempts to populate the `Label` field for the current channel by reading off the corresponding entry in the `sADCChannelName` array of the `ABFFileHeader` structure pointed to by `hdr->AS.Header`.

To better understand the cause of this vulnerability, let’s analyze each of the arguments passed to `strncpy`. We’ll start with the simplest: the number of bytes to copy. In this case, it is set to the variable `ll`, which is defined as the smaller of two constants: `ABF_ADCNAMELEN` and `MAX_LENGTH_LABEL` [7]. Since abfheadr.h defines `ABF_ADCNAMELEN` as 10:

“`
#define ABF_ADCNAMELEN 10 // length of ADC channel name strings
“`

and biosig-dev.h defines `MAX_LENGTH_LABEL` as 80:

“`
#define MAX_LENGTH_LABEL 80 // TMS: 40, AXG: 79
“`

we know that `ll`, and thus the total number of bytes copied, is 10 (0xa).

Next, the destination is `hc->Label`, which refers to the `Label` member of the structure pointed to by `hc`. The `Label` member is defined as a character array of size `MAX_LENGTH_LABEL+1` (81) in biosig-dev.h:

“`
char Label[MAX_LENGTH_LABEL+1] ATT_ALI; /* Label of channel */
“`

`hc`, on the other hand, is defined at the very start of the for loop [8] as a pointer to the `CHANNEL_TYPE` entry in the `hdr->CHANNEL` array for the current ADC channel being processed, whose index is `k1`. Thus, the destination for the `strncpy` call is the `Label` field for the ADC channel in the `hdr->CHANNEL` array with index `k1`.

The source is a bit more complicated, being the result of the following expression:

`(char*)hdr->AS.Header + offsetof(struct ABFFileHeader, sADCChannelName) + nss*ABF_ADCNAMELEN`

The expression first computes the offset of the `sADCChannelName` member of the `ABFFileHeader` structure pointed to by `hdr->AS.Header`, and then offsets it by `nss*ABF_ADCNAMELEN`. Looking at the definition of `ABFFileHeader` located in abfheadr.h, `sADCChannelName` is an array of strings (implented as a two-dimensional char array) with `ABF_ADCCOUNT` entries, and each entry having a fixed size of `ABF_ADCNAMELEN`:

“`
char sADCChannelName[ABF_ADCCOUNT][ABF_ADCNAMELEN];
“`

Thus, the source is the address of the `nss` th entry in this `sADCChannelName` array. The question then becomes, where does `nss` come from? `nss` is actually initialized just before the call to `strncpy` [9] via a call to `lei16p`, which reads a signed 16-bit integer from a given address:

“`
static inline int16_t lei16p(const void* i) { uint16_t a; memcpy(&a, i, sizeof(a)); return ((int16_t)le16toh(a)); }
“`

In this case, the address read by `lei16p` is the result of the following expression:

`hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k`

The expression first computes the offset of the `nADCSamplingSeq` member of the `ABFFileHeader` structure pointed to by `hdr->AS.Header`, and then offsets it by `2 * k`. Looking at the definition of `ABFFileHeader` located in abfheadr.h, `nADCSamplingSeq` is an array of shorts (16-bit integers) with `ABF_ADCCOUNT` entries:

“`
short nADCSamplingSeq[ABF_ADCCOUNT];
“`

Since each entry in this array is two bytes, an offset of `2 * k` corresponds to the `k` th entry. Thus, `nss` is computed by reading the `k` th entry of `hdr->AS.Header`’s `nADCSamplingSeq` array.

This computation of `nss` is actually the root cause of the out-of-bounds read. As stated previously, `nss` is used as the index for the entry in the `sADCChannelName` array that `strncpy` attempts to copy into `hc->Label`. We also know that the `sADCChannelName` array has a total of `ABF_ADCCOUNT` entries, which is defined to be 16 in abfheadr.h:

“`
#define ABF_ADCCOUNT 16 // number of ADC channels supported.
“`

`nss`, on other hand, is computed by reading a signed 16-bit integer from the `nADCSamplingSeq` array, which is populated using data from the input file provided to libbiosig and is thus attacker-controlled. This means that `nss` can take on any signed 16-bit value and can therefore greatly exceed 16, the maximum number of the entries in the `sADCChannelName` array. Since libbiosig does not perform any checks to ensure that `nss` is less than 16 before using it to compute the source address for `strncpy`, an out-of-bounds read condition is made possible.

This can be confirmed by supplying the attached POC (where such a condition occurs) to libbiosig as input and attaching GDB:

“`
Breakpoint 2, sopen_abf_read (hdr=hdr@entry=0x55555556e920) at ./t210/sopen_abf_read.c:410 410 if ((k < ABF_ADCCOUNT) && (lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k) >= 0)) { ────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────── In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/t210/sopen_abf_read.c:410 405 406 hdr->CHANNEL = realloc(hdr->CHANNEL, hdr->NS*sizeof(CHANNEL_TYPE)); 407 uint32_t k1=0,k; // ABF is 32 bits only, no need for more 408 for (k=0; k < ABF_ADCCOUNT + ABF_DACCOUNT; k++) { 409 CHANNEL_TYPE *hc = hdr->CHANNEL+k1; ► 410 if ((k < ABF_ADCCOUNT) && (lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k) >= 0)) { 411 hc->OnOff = 1; 412 hc->bufptr = NULL; 413 hc->LeadIdCode = 0; 414 int ll = min(ABF_ADCNAMELEN, MAX_LENGTH_LABEL); 415 int16_t nss = lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k); ──────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────── ► 0 0x7ffff7ea6b20 sopen_abf_read+736 1 0x7ffff7e7f912 sopen_extended+30546 2 0x555555555283 main+186 3 0x7ffff762a1ca __libc_start_call_main+122 4 0x7ffff762a28b __libc_start_main+139 5 0x555555555105 _start+37
“`

By setting a breakpoint shortly before the vulnerable call to `strncpy`, we can retrieve the addresses of `hc->Label` [10] and `hdr->AS.Header` [11], as well as inspect the heap to see `hdr->AS.Header`’s currently allocated size [12]:

“`
pwndbg> p/x &(hc->Label) $1 = 0x7ffff4f4d040 // [10] pwndbg> p/x hdr->AS.Header $2 = 0x5555555717e0 // [11] pwndbg> heap -v 0x5555555717d0 Allocated chunk | PREV_INUSE Addr: 0x5555555717d0 prev_size: 0x00 size: 0x1010 (with flag bits: 0x1011) // [12] fd: 0x2046444720464241 bk: 0xfd9df90435322e31 fd_nextsize: 0xfd12fc37fd37ff8d bk_nextsize: 0x3730303220
“`

Thus, `hdr->AS.Header` has an allocated size of 0x1010 (4112) bytes. By stepping a few more times, we can inspect the final state of the arguments passed to `strncpy`, as well as retrieve the value of `nss` right before the call is made:

“`
0x00007ffff7ea7a11 95 return __builtin___strncpy_chk (__dest, __src, __len, ──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────── 0x7ffff7ea79f8 lea esi, [rax + rax + 0x1ba] ESI => 0x142fa 0x7ffff7ea79ff mov dword ptr [rbp – 0x98], r8d [0x7fffffffcc88] <= 0x2020 0x7ffff7ea7a06 movsxd rsi, esi RSI => 0x142fa 0x7ffff7ea7a09 add rsi, rdx RSI => 0x555555585ada (0x142fa + 0x5555555717e0) 0x7ffff7ea7a0c mov edx, 0xa EDX => 0xa ► 0x7ffff7ea7a11 call strncpy@plt dest: 0x7ffff4f4d040 ◂— 0 // [13] src: 0x555555585ada // [14] n: 0xa // [15] 0x7ffff7ea7a16 mov edx, 0xa EDX => 0xa 0x7ffff7ea7a1b mov qword ptr [rbp – 0xa8], rbx 0x7ffff7ea7a22 mov rbx, rdx 0x7ffff7ea7a25 jmp sopen_abf_read+4624 ↓ 0x7ffff7ea7a50 test rbx, rbx ──────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────── ► 0 0x7ffff7ea7a11 sopen_abf_read+4561 1 0x7ffff7ea7a11 sopen_abf_read+4561 2 0x7ffff7e7f912 sopen_extended+30546 3 0x555555555283 main+186 4 0x7ffff762a1ca __libc_start_call_main+122 5 0x7ffff762a28b __libc_start_main+139 6 0x555555555105 _start+37 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> p/x nss $2 = 0x2020 // [16]
“`

The size [15] is 0xa (10), as expected. The destination address [13] matches the address of `hc->Label` [10] we computed earlier. The source address of 0x555555585ada [14], as expected, is equal to the address of `hdr->AS.Header` (0x5555555717e0) plus the offset of the `sADCChannelName` member (0x1ba) plus `nss*ABF_ADCNAMELEN` (0x2020 * 0xa = 0x14140). Since the value of `nss` [16] exceeds 16, `strncpy` will attempt to read past the end of the `sADCChannelName` array. In fact, the source address about to be passed to `strncpy` actually exceeds the bounds of the total memory allocated to `hdr->AS.Header` on the heap, since 0x5555555717e0 + 0x1010 = 0x5555555727f0 < 0x555555585ada.

Unsuprisingly, continuing execution from here results in a segfault as `strncpy` attempts to read the out-of-bounds source address:

“`
Program received signal SIGSEGV, Segmentation fault. __strncpy_evex () at ../sysdeps/x86_64/multiarch/strncpy-evex.S:112 warning: 112 ../sysdeps/x86_64/multiarch/strncpy-evex.S: No such file or directory LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA ─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────── *RAX 0xada RBX 0x55555556e920 —▸ 0x55555556f490 ◂— ‘/home/mbereza/Share/Cisco/abf_read_oob_heap_write_poc’ RCX 0 *RDX 9 *RDI 0x7ffff4f4d040 ◂— 0 *RSI 0x555555585ada *R8 0x2020 R9 7 *R10 0x19a R11 0x15d879cd5e791d69 R12 0 R13 0 R14 0x7ffff4f4d010 ◂— 0 R15 0 RBP 0x7fffffffcd20 —▸ 0x7fffffffd620 —▸ 0x7fffffffd670 —▸ 0x7fffffffd710 —▸ 0x7fffffffd770 ◂— … *RSP 0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa *RIP 0x7ffff779d0a5 (__strncpy_evex+37) ◂— vmovdqu64 ymm16, ymmword ptr [rsi] ──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────── ► 0x7ffff779d0a5 <__strncpy_evex+37> vmovdqu64 ymm16, ymmword ptr [rsi] 0x7ffff779d0ab <__strncpy_evex+43> vptestnmb k0, ymm16, ymm16 0x7ffff779d0b1 <__strncpy_evex+49> kmovd ecx, k0 0x7ffff779d0b5 <__strncpy_evex+53> mov rax, rdi 0x7ffff779d0b8 <__strncpy_evex+56> cmp rdx, 0x20 0x7ffff779d0bc <__strncpy_evex+60> jb __strncpy_evex+1152 <__strncpy_evex+1152> 0x7ffff779d0c2 <__strncpy_evex+66> vmovdqu64 ymmword ptr [rdi], ymm16 0x7ffff779d0c8 <__strncpy_evex+72> test ecx, ecx 0x7ffff779d0ca <__strncpy_evex+74> jne __strncpy_evex+855 <__strncpy_evex+855> 0x7ffff779d0d0 <__strncpy_evex+80> lea rdx, [rsi + rdx – 0x20] 0x7ffff779d0d5 <__strncpy_evex+85> sub rdi, rsi ────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa 01:0008│-0c0 0x7fffffffcc60 ◂— 0 02:0010│-0b8 0x7fffffffcc68 ◂— 0 03:0018│-0b0 0x7fffffffcc70 ◂— 0x3000000000000 04:0020│-0a8 0x7fffffffcc78 ◂— 0 05:0028│-0a0 0x7fffffffcc80 ◂— 0x2020202000000000 06:0030│-098 0x7fffffffcc88 ◂— 0x2020202000002020 /* ‘ ‘ */ 07:0038│-090 0x7fffffffcc90 ◂— 0x19a ──────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────── ► 0 0x7ffff779d0a5 __strncpy_evex+37 1 0x7ffff7ea7a16 sopen_abf_read+4566 2 0x7ffff7ea7a16 sopen_abf_read+4566 3 0x7ffff7e7f912 sopen_extended+30546 4 0x555555555283 main+186 5 0x7ffff762a1ca __libc_start_call_main+122 6 0x7ffff762a28b __libc_start_main+139 7 0x555555555105 _start+37 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
“`

As mentioned previously, the source address for this out-of-bounds read is attacker-controlled. Additionally, the destination is also in part controlled by attacker, as `hc` points to the `CHANNEL_TYPE` structure for the current channel, and the attacker can pad the input file with one or more benign channels before the one that ultimately triggers the vulnerability.

“`
Program received signal SIGSEGV, Segmentation fault. __strncpy_evex () at ../sysdeps/x86_64/multiarch/strncpy-evex.S:112 warning: 112 ../sysdeps/x86_64/multiarch/strncpy-evex.S: No such file or directory LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA ─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────── *RAX 0xada RBX 0x55555556e920 —▸ 0x55555556f490 ◂— ‘/home/mbereza/Share/Cisco/abf_read_oob_heap_write_poc’ RCX 0 *RDX 9 *RDI 0x7ffff4f4d040 ◂— 0 *RSI 0x555555585ada *R8 0x2020 R9 7 *R10 0x19a R11 0x15d879cd5e791d69 R12 0 R13 0 R14 0x7ffff4f4d010 ◂— 0 R15 0 RBP 0x7fffffffcd20 —▸ 0x7fffffffd620 —▸ 0x7fffffffd670 —▸ 0x7fffffffd710 —▸ 0x7fffffffd770 ◂— … *RSP 0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa *RIP 0x7ffff779d0a5 (__strncpy_evex+37) ◂— vmovdqu64 ymm16, ymmword ptr [rsi] ──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────── ► 0x7ffff779d0a5 <__strncpy_evex+37> vmovdqu64 ymm16, ymmword ptr [rsi] 0x7ffff779d0ab <__strncpy_evex+43> vptestnmb k0, ymm16, ymm16 0x7ffff779d0b1 <__strncpy_evex+49> kmovd ecx, k0 0x7ffff779d0b5 <__strncpy_evex+53> mov rax, rdi 0x7ffff779d0b8 <__strncpy_evex+56> cmp rdx, 0x20 0x7ffff779d0bc <__strncpy_evex+60> jb __strncpy_evex+1152 <__strncpy_evex+1152> 0x7ffff779d0c2 <__strncpy_evex+66> vmovdqu64 ymmword ptr [rdi], ymm16 0x7ffff779d0c8 <__strncpy_evex+72> test ecx, ecx 0x7ffff779d0ca <__strncpy_evex+74> jne __strncpy_evex+855 <__strncpy_evex+855> 0x7ffff779d0d0 <__strncpy_evex+80> lea rdx, [rsi + rdx – 0x20] 0x7ffff779d0d5 <__strncpy_evex+85> sub rdi, rsi ────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa 01:0008│-0c0 0x7fffffffcc60 ◂— 0 02:0010│-0b8 0x7fffffffcc68 ◂— 0 03:0018│-0b0 0x7fffffffcc70 ◂— 0x3000000000000 04:0020│-0a8 0x7fffffffcc78 ◂— 0 05:0028│-0a0 0x7fffffffcc80 ◂— 0x2020202000000000 06:0030│-098 0x7fffffffcc88 ◂— 0x2020202000002020 /* ‘ ‘ */ 07:0038│-090 0x7fffffffcc90 ◂— 0x19a ──────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────── ► 0 0x7ffff779d0a5 __strncpy_evex+37 1 0x7ffff7ea7a16 sopen_abf_read+4566 2 0x7ffff7ea7a16 sopen_abf_read+4566 3 0x7ffff7e7f912 sopen_extended+30546 4 0x555555555283 main+186 5 0x7ffff762a1ca __libc_start_call_main+122 6 0x7ffff762a28b __libc_start_main+139 7 0x555555555105 _start+37 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
“`

2026-01-12 – Vendor Disclosure

2026-02-15 – Vendor Patch Release

2026-03-03 – Public Release

Discovered by Mark Bereza of Cisco Talos.