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,"\x81\xa4\xb1\xf3",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 returns 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.