CVE-2026-22891
A heap-based buffer overflow vulnerability exists in the Intan CLP parsing functionality of The Biosig Project libbiosig 3.9.2 and Master Branch (db9a9a63). A specially crafted Intan CLP file can lead to arbitrary code execution. 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 (db9a9a63)
libbiosig – https://biosig.sourceforge.net/index.html
9.8 – CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-122 – Heap-based Buffer Overflow
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 Intan Technologies CLP file format, a data file format for encoding signals recorded using the Intan CLAMP System, which their user guide describes as a “patch clamp amplifier system [which] allows users to perform signal channel or multi-channel patch clamp electrophysiology or electrochemistry experiments using the revolutionary new Intan CLAMP voltage/current clamp chips.”
To determine if the input file is using the Intan CLP format, `getfiletype` runs the following check:
“`
else if (!memcmp(Header1,”x81xa4xb1xf3″,4) & (leu16p(Header1+8) < 2)) { hdr->TYPE = IntanCLP; // Intan CLP format, we’ll use same read for now hdr->FILE.LittleEndian = 1; }
“`
Put simply, libbiosig classifies an input file as Intan CLP if the first four bytes match the magic byte sequence 0x81A4B1F3, and if the 9th byte (which encodes the data type) is less than 2, as this field can only take on a value of 0 or 1. The file classification is then stored in the struct member `hdr->TYPE`.
Further along in `sopen_extended`, after `getfiletype` has returned, `hdr->TYPE` is checked again and, if it’s `IntanCLP`, file processing is handed off to the dedicated function `sopen_intan_clp_read`, found in sopen_rhd2000_read.c:
“`
else if (hdr->TYPE==IntanCLP) { sopen_intan_clp_read(hdr); }
“`
`sopen_intan_clp_read` starts by reading various fields from the RHS2000 file header and storing them in local variables:
“`
int sopen_intan_clp_read(HDRTYPE* hdr) { uint16_t NumADCs=0, NumChips=0, NumChan=0; float minor = leu16p(hdr->AS.Header+6); minor *= (minor < 10) ? 0.1 : 0.01; hdr->VERSION = leu16p(hdr->AS.Header+4) + minor; uint16_t datatype=leu16p(hdr->AS.Header+8); // [1] switch (datatype) { case 1: NumADCs=leu16p(hdr->AS.Header+10); hdr->SampleRate = lef32p(hdr->AS.Header+24); case 0: break; default: // this should never ever occurs, because getfiletype checks this biosigERROR(hdr, B4C_FORMAT_UNSUPPORTED, “Format Intan CLP – datatype unknown”); return -1; }
“`
Of these, the only one of relevance to this vulnerability is `datatype` [1], which encodes whether the file contains standard data ( `datatype = 0`) or auxiliary data ( `datatype = 1`).
Next, the function parses the length field and attempts to read the header:
“`
size_t HeadLen=leu16p(hdr->AS.Header+10+(datatype*2)); // [2] // read header if (HeadLen > hdr->HeadLen) { // [3] hdr->AS.Header = (uint8_t*)realloc(hdr->AS.Header, HeadLen+1); // [5] hdr->HeadLen += ifread(hdr->AS.Header+HeadLen, 1, HeadLen – hdr->HeadLen, hdr); // [4] } hdr->AS.Header[hdr->HeadLen]=0; // [10] if (HeadLen > hdr->HeadLen) { biosigERROR(hdr, B4C_FORMAT_UNSUPPORTED, “Format Intan/CLP – file is too short”); return -1; } ifseek(hdr, HeadLen, SEEK_SET);
“`
The file’s self-advertised length in bytes is encoded as a 16-bit unsigned integer, which the function stores in the local variable `HeadLen`[2]. When dealing with a standard data file, `sopen_intan_clp_read` looks for the length at an offset of 10, whereas auxiliary data files instead store `NumADCs` at offset 10 and length at offset 12.
From there, the function compares this read length against `hdr->HeadLen` [3], which corresponds to the number of bytes already read from the input file thus far. If `HeadLen` is larger (it should be, as the Intan CLP data hasn’t been processed yet), `sopen_intan_clp_read` attempts to read all remaining unread bytes ( `HeadLen – hdr->HeadLen`) into `heap->AS.Header` [4]. Since `heap->AS.Header` is a heap-allocated buffer, it is first resized to accomodate the length read from the CLP header via a call to `realloc` [5], with the new size being `HeadLen+1` bytes.
The data is read from the file into `hdr->AS.Header` via a call for `ifread`, which is simply a wrapper for `fread` that uses `hdr->FILE.FID` (file descriptor for the input file to libbiosig) as the stream to be read:
“`
size_t ifread(void* ptr, size_t size, size_t nmemb, HDRTYPE* hdr) { #ifdef ZLIB_H . . . #endif return(fread(ptr, size, nmemb, hdr->FILE.FID)); }
“`
The vulnerability occurs during this call to `ifread`, a direct result of the wrong value being supplied to `fread`’s first argument, which is a pointer to the buffer where the read bytes should be written to. In this case, the pointer chosen is `hdr->AS.Header+HeadLen`, which is `HeadLen` bytes past the start of the `hdr->AS.Header` buffer. `HeadLen`, as mentioned previously, is the length read from the Intan CLP header and, critically, the new length allocated for `hdr->AS.Header`. This means that the read bytes are copied starting at the very **end** of the `hdr->AS.Header` buffer, resulting in every byte except the first being written out-of-bounds.
This can be demonstrated dynamically by supplying the attached POC file to libbiosig and attaching a debugger:
“`
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────── In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/t210/sopen_rhd2000_read.c:112 107 108 size_t HeadLen=leu16p(hdr->AS.Header+10+(datatype*2)); 109 // read header 110 if (HeadLen > hdr->HeadLen) { 111 hdr->AS.Header = (uint8_t*)realloc(hdr->AS.Header, HeadLen+1); ► 112 hdr->HeadLen += ifread(hdr->AS.Header+HeadLen, 1, HeadLen – hdr->HeadLen, hdr); 113 } 114 hdr->AS.Header[hdr->HeadLen]=0; 115 if (HeadLen > hdr->HeadLen) { 116 biosigERROR(hdr, B4C_FORMAT_UNSUPPORTED, “Format Intan/CLP – file is too short”); 117 return -1; ──────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────── ► 0 0x7ffff73c9834 sopen_intan_clp_read+4004 1 0x7ffff7308fcf sopen_extended+57295 2 0x5555555554c8 main+511 3 0x7ffff6a2a1ca __libc_start_call_main+122 4 0x7ffff6a2a28b __libc_start_main+139 5 0x555555555205 _start+37 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> p/x datatype $1 = 0x1 // [6] pwndbg> p/x HeadLen $2 = 0x7e43 // [7] pwndbg> p/x hdr->AS.Header $3 = 0x52d000000400 pwndbg> heap -v 0x52d0000003f0 Allocated chunk | PREV_INUSE Addr: 0x52d0000003f0 size: 0x7e50 (with flag bits: 0x7e51) // [8]
“`
By halting execution right before the vulnerability is triggered, we can inspect the final state of relevant variables and buffers. Since `datatype` is 1 [6], the POC is being treated as an auxiliary data file and thus `HeadLen` was read from offset 12 into the file header. The `HeadLen` read from the POC ended up being 0x7e43 (32,323) [7], meaning that the reallocated size of `hdr->AS.Header` should be one more than that: 0x7e44 bytes. Inspecting the heap chunk allocated for `hdr->AS.Header` reveals this to be more or less the case, with the chunk size being 0x7e50 (32,336) [8], the difference likely due to word-alignment and heap metadata overhead. Thus any write past `0x52d0000003f0 (start of heap chunk) + 0x7e50 (size of heap chunk) = 0x52d000008240` will result in a heap-based buffer overflow condition.
Sure enough, attempting to step forward from here immediately triggers AddressSanitizer with a heap-buffer-overflow condition:
“`
==50282==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x52d000008244 at pc 0x7ffff787ed7f bp 0x7fffffffa610 sp 0x7fffffff9db8 // [9] WRITE of size 9459 at 0x52d000008244 thread T0 #0 0x7ffff787ed7e in fread ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996 #1 0x7ffff73c9887 in sopen_intan_clp_read t210/sopen_rhd2000_read.c:112 #2 0x7ffff7308fce in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10869 #3 0x5555555554c7 in main harness.cpp:38 #4 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #5 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360 #6 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3) 0x52d000008244 is located 0 bytes after 32324-byte region [0x52d000000400,0x52d000008244) allocated by thread T0 here: #0 0x7ffff78fc778 in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85 #1 0x7ffff73c97f2 in sopen_intan_clp_read t210/sopen_rhd2000_read.c:111 #2 0x7ffff7308fce in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10869 #3 0x5555555554c7 in main harness.cpp:38 #4 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #5 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360 #6 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3) SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996 in fread Shadow bytes around the buggy address: 0x52d000007f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x52d000008200: 00 00 00 00 00 00 00 00[04]fa fa fa fa fa fa fa 0x52d000008280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==50282==ABORTING [Inferior 1 (process 50282) exited with code 01]
“`
The AddressSantizer output reveals that the overflow occurred at the address 0x52d000008244 [9], which is just past the end of the heap chunk allocated for `hdr->AS.Header`.
It is likely that the intended destination for this call to `ifread` was `hdr->AS.Header+hdr->HeadLen`, which would correspond to current position in the file stream (one byte past the last byte read). Since the number of bytes to be copied is `HeadLen – hdr->HeadLen`, this would result in the final byte being written just before end of the newly-allocated size ( `hdr->AS.Header[Headlen-1]`), followed by a null terminator immediately after that [10].
It should be noted that the source code for `sopen_intan_clp_read` seems to indicate that the Intan CLP file format is not officially supported:
“`
biosigERROR(hdr, B4C_FORMAT_UNSUPPORTED, “Format Intan/CLP not supported”); return -1; }
“`
This block is found right at the very end of the function, with no other `return` s present anywhere in the function. Thus, attempting to process an Intan CLP file with libbiosig should **always** produce an error. However, because the error is generated at the very end of the function, the vulnerable code is still reachable and thus represents a legitimate attack vector.
Since the data written by `fread` comes from the input file, this vulnerability allows for a heap-based buffer overflow where the data written is controlled by the attacker. Depending on the setup of the heap, this flaw can potentially lead to arbitrary code execution.
“`
==50282==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x52d000008244 at pc 0x7ffff787ed7f bp 0x7fffffffa610 sp 0x7fffffff9db8 WRITE of size 9459 at 0x52d000008244 thread T0 #0 0x7ffff787ed7e in fread ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996 #1 0x7ffff73c9887 in sopen_intan_clp_read t210/sopen_rhd2000_read.c:112 #2 0x7ffff7308fce in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10869 #3 0x5555555554c7 in main harness.cpp:38 #4 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #5 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360 #6 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3) 0x52d000008244 is located 0 bytes after 32324-byte region [0x52d000000400,0x52d000008244) allocated by thread T0 here: #0 0x7ffff78fc778 in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85 #1 0x7ffff73c97f2 in sopen_intan_clp_read t210/sopen_rhd2000_read.c:111 #2 0x7ffff7308fce in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10869 #3 0x5555555554c7 in main harness.cpp:38 #4 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #5 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360 #6 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3) SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996 in fread Shadow bytes around the buggy address: 0x52d000007f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x52d000008180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x52d000008200: 00 00 00 00 00 00 00 00[04]fa fa fa fa fa fa fa 0x52d000008280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x52d000008480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==50282==ABORTING [Inferior 1 (process 50282) exited with code 01]
“`
2026-02-12 – Vendor Disclosure
2026-02-15 – Vendor Patch Release
2026-03-03 – Public Release
Discovered by Mark Bereza and Lilith >_> of Cisco Talos.
