CVE-2025-48005
A heap-based buffer overflow vulnerability exists in the RHS2000 parsing functionality of The Biosig Project libbiosig 3.9.0 and Master Branch (35a819fa). A specially crafted RHS2000 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.0
The Biosig Project libbiosig Master Branch (35a819fa)
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 RHS2000 file format, a data file format for encoding electrophysiological signals recorded using Intan’s RHS2000 family of devices.
To determine if the input file is valid RHS2000, getfiletype
runs the following check:
else if (!memcmp(Header1,"\xAC\x27\x91\xD6",4)) {
hdr->TYPE = RHS2000; // Intan RHS2000 format
hdr->FILE.LittleEndian = 1;
}
Put simply, libbiosig classifies an input file as RHS2000 if the first four bytes matches the magic byte sequence 0xAC2791D6 and stores the file classification in the struct member hdr->TYPE
.
Further along in sopen_extended
, after getfiletype
has returned, hdr->TYPE
is checked again and, if it’s RHS2000, file processing is handed off to the dedicated function sopen_rhs2000_read
:
else if (hdr->TYPE==RHS2000) {
sopen_rhs2000_read(hdr);
}
sopen_rhs2000_read
starts by reading various fields from the RHS2000 file header and storing them in local variables:
/*
RHS2000 Data File Formats - Intan Tech
http://www.intantech.com/files/Intan_RHS2000_data_file_formats.pdf
*/
int sopen_rhs2000_read(HDRTYPE* hdr) {
if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %u); %s(...) [%u]\n",__FILE__,__LINE__,__func__,hdr->HeadLen);
// 8 bytes
int16_t major = leu16p(hdr->AS.Header+4);
int16_t minor = leu16p(hdr->AS.Header+6);
hdr->VERSION = major + minor * (minor < 10 ? 0.1 : 0.01);
// +38 = 46 bytes
hdr->NS = 1;
hdr->SampleRate = lef32p(hdr->AS.Header+8);
float HighPass = ( leu16p(hdr->AS.Header+12) ? lef32p(hdr->AS.Header+14) : 0.0 );
HighPass = max( HighPass, lef32p(hdr->AS.Header+18) );
float LowPass = lef32p(hdr->AS.Header+26);
...
uint16_t numberOfSignalGroups = leu16p(hdr->AS.Header+pos); // [1]
pos += 2;
uint16_t NS = 0; // [2]
It should be noted that the numberOfSignalGroups
is a 16-bit integer that is read from the input file [1] - this value will become relevant shortly. Additionally, the local variable NS
is initialized to 0 [2] - this will be used to store the current number of channels.
The bulk of the processing, however, seems to occur further into the function, in a nested for loop:
// read all signal groups
for (int nsg=0; nsg < numberOfSignalGroups; nsg++) { // [3]
char SignalGroupName[101], SignalGroupPrefix[101];
read_qstring(hdr, &pos, SignalGroupName, 100);
read_qstring(hdr, &pos, SignalGroupPrefix, 100);
if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %u); %s(...) group=%u %u SGP<%s> SGP<%s>\n",__FILE__,__LINE__,__func__, nsg, (unsigned)pos, SignalGroupName,SignalGroupPrefix );
uint16_t flag_SignalGroupEnabled = leu16p(hdr->AS.Header+pos); // [4]
pos += 2;
uint16_t NumberOfChannelsInSignalGroup = leu16p(hdr->AS.Header+pos); // [5]
pos += 2;
uint16_t NumberOfAmplifierChannelsInSignalGroup = leu16p(hdr->AS.Header+pos);
pos += 2;
if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %u); %s(...) group=%u %u+%u %d\n", __FILE__, __LINE__, __func__, nsg, NS, NumberOfChannelsInSignalGroup, (int)pos);
if (flag_SignalGroupEnabled) { // [6]
typeof(pos) tmppos = pos;
int NumChans0 = 0;
int NumChans1 = 0;
// get number of enabled channels
for (unsigned k = 0; k < NumberOfChannelsInSignalGroup; k++) {
read_qstring(hdr, &tmppos, NULL, 0);
read_qstring(hdr, &tmppos, NULL, 0);
tmppos += 4;
int16_t SignalType = lei16p(hdr->AS.Header+tmppos);
tmppos += 2;
int16_t ChannelEnabled = lei16p(hdr->AS.Header+tmppos);
tmppos += 24;
NumChans0 += (ChannelEnabled > 0); // [7]
NumChans1 += (ChannelEnabled > 0) * (1 + (SignalType==0) * (flag_DC_amplifier_data_saved+1)); // [8]
}
hdr->CHANNEL = (CHANNEL_TYPE*) realloc(hdr->CHANNEL, (1+NS+NumChans1) * sizeof(CHANNEL_TYPE)); // [9]
for (unsigned k = 0; k < NumberOfChannelsInSignalGroup; k++) { // [10]
char NativeChannelName[MAX_LENGTH_LABEL+1];
...
The outer for loop [3] appears to iterate over the signal groups contained within the file, using the numberOfSignalGroups
value read from the input file earlier as its upper bound. Each of these signal groups seems to have its own dedicated fields, which the body of this loop reads from the file and stores in local variables. Of particular interest are the fields flag_SignalGroupEnabled
[4] and NumberOfChannelsInSignalGroup
[5], which encode the enabled/disabled state and the number of channels for the current signal group, respectively.
For signal groups that are marked as enabled via flag_SignalGroupEnabled
[6], sopen_rhs2000_read
performs additional processing, which includes counting the total number of enabled channels in the group and storing it two different ways: NumChans0
appears to hold a simple count of the total number of enabled channels in the current signal group [7], while NumChans1
increments the count additional times for channels with a SignalType
of 0 [8]. Based on its use further in the code, channels with a SignalType
of 0 seem to encode additional data channels relative to those with a nonzero SignalType
, potentially explaining this distinction. NumChans1
is used when resizing hdr->CHANNEL
shortly thereafter [9], with the new size accomodating a number of channels equal to 1+NS+NumChans1
. With the channel counts recorded and hdr->CHANNEL
resized accordingly, a second for loop is then used to iterate over the channels in the current signal group [10].
For context, hdr->CHANNEL
is a heap allocated array of CHANNEL_TYPE
structures that libbiosig uses to store channel data, with each channel getting its own entry in this array. The CHANNEL_TYPE
(aka CHANNEL_STRUCT
) structure itself is defined in biosig-dev.h:
typedef struct CHANNEL_STRUCT {
double PhysMin ATT_ALI; /* physical minimum */
double PhysMax ATT_ALI; /* physical maximum */
double DigMin ATT_ALI; /* digital minimum */
double DigMax ATT_ALI; /* digital maximum */
double Cal ATT_ALI; /* gain factor */
double Off ATT_ALI; /* bias */
char Label[MAX_LENGTH_LABEL+1] ATT_ALI; /* Label of channel */
char OnOff ATT_ALI; /* 0: channel is off, not consider for data output; 1: channel is turned on; 2: channel containing time axis */
uint16_t LeadIdCode ATT_ALI; /* Lead identification code */
char Transducer[MAX_LENGTH_TRANSDUCER+1] ATT_ALI; /* transducer e.g. EEG: Ag-AgCl electrodes */
#ifdef MAX_LENGTH_PHYSDIM
char PhysDim[MAX_LENGTH_PHYSDIM+1] ATT_ALI ATT_DEPREC; /* DONOT USE - use PhysDim3(PhysDimCode) instead */
#endif
uint16_t PhysDimCode ATT_ALI; /* code for physical dimension - PhysDim3(PhysDimCode) returns corresponding string */
float TOffset ATT_ALI; /* time delay of sampling */
float LowPass ATT_ALI; /* lowpass filter */
float HighPass ATT_ALI; /* high pass */
float Notch ATT_ALI; /* notch filter */
float XYZ[3] ATT_ALI; /* sensor position */
union {
/* context specific channel information */
float Impedance ATT_ALI; /* Electrode Impedance in Ohm, defined only if PhysDim = _Volt */
float fZ ATT_ALI; /* ICG probe frequency, defined only if PhysDim = _Ohm */
} ATT_ALI;
/* this part should not be used by application programs */
uint8_t* bufptr ATT_ALI; /* pointer to buffer: NRec<=1 and bi,bi8 not used */
uint32_t SPR ATT_ALI; /* samples per record (block) */
uint32_t bi ATT_ALI; /* start byte (byte index) of channel within data block */
uint32_t bi8 ATT_ALI; /* start bit (bit index) of channel within data block */
uint16_t GDFTYP ATT_ALI; /* data type */
} CHANNEL_TYPE ATT_ALI ATT_MSSTRUCT;
After the outer for loop completes (the one that iterates over the signal groups), some special processing is performed for the time channel, which corresponds to index 0 in the hdr->CHANNEL
array:
{ // channel 0 - time channel
CHANNEL_TYPE *hc = hdr->CHANNEL+0; // [11]
hc->OnOff = 2;
strcpy(hc->Label, "time");
strcpy(hc->Transducer, "");
#ifdef MAX_LENGTH_PHYSDIM
strcpy(hc->PhysDim,"s");
#endif
hc->bi = 0;
hc->bufptr = NULL;
hc->OnOff = 2; // time channel
hc->SPR = hdr->SPR;
hc->GDFTYP = 6; // uint32
hc->bi = 0;
hc->bi8 = hc->bi << 3;
hc->LeadIdCode = 0;
hc->DigMin = 0.0;
hc->DigMax = ldexp(1,32)-1;
hc->Off = 0.0;
hc->Cal = 1.0/hdr->SampleRate;
hc->PhysDimCode = 2176; // [s]
hc->PhysMin = 0;
hc->PhysMax = hc->DigMax*hc->Cal;
hc->bufptr = NULL;
hc->TOffset = 0;
hc->LowPass = 0;
hc->HighPass = INFINITY;
hc->Notch = 0;
hc->XYZ[0] = NAN;
hc->XYZ[1] = NAN;
hc->XYZ[2] = NAN;
hc->Impedance = NAN;
}
Note: For simplicity, the code block above will be referred to as the “time channel code” from here on.
Within the time channel code, hc
is a handle to the CHANNEL_TYPE
structure that stores the time channel data within the larger hdr->CHANNEL
array [11], and this handle is then used to populate the structure’s various fields. Unfortunately, this leads to numerous heap-based buffer overflow vulnerabilties due to the initial size allocated for the hdr->CHANNEL
array being too small to fit even a single CHANNEL_TYPE
structure. While sopen_rhs2000_read
does attempt to resize hdr->CHANNEL
based on the number of enabled channels discovered in the input file [9], that code is never hit in cases where numberOfSignalGroups
is 0 [3], or in cases where flag_SignalGroupEnabled
is 0 for every signal group [6].
To demonstrate why the initial size of hdr->CHANNEL
is insufficient to store the CHANNEL_TYPE
structure for the time channel, we’ll refer to constructHDR
, the function used to initialize hdr
:
/****************************************************************************/
/** INIT HDR **/
/****************************************************************************/
#define Header1 ((char*)hdr->AS.Header)
HDRTYPE* constructHDR(const unsigned NS, const unsigned N_EVENT)
{
/*
HDR is initialized, memory is allocated for
NS channels and N_EVENT number of events.
The purpose is to define all parameters at an initial step.
No parameters must remain undefined.
*/
HDRTYPE* hdr = (HDRTYPE*)malloc(sizeof(HDRTYPE));
...
hdr->NS = NS; // [13]
...
// define variable header
hdr->CHANNEL = (CHANNEL_TYPE*)calloc(hdr->NS, sizeof(CHANNEL_TYPE)); // [12]
...
return(hdr);
}
From the definition of constructHDR
, we can see that the memory for the hdr->CHANNEL
array is allocated on the heap using a call to calloc
[12], with sizeof(CHANNEL_TYPE)
used as the size of each element and hdr->NS
as the number of elements. Further up in constructHDR
, hdr->NS
is initialized to NS
[13], the first argument passed to the function, so it is this argument that ultimately controls the allocated size of hdr->CHANNEL
.
Looking at the only place this function is called within biosig.c, we can see that this argument is set to 0:
if (hdr==NULL)
hdr = constructHDR(0,0); // initializes fields that may stay undefined during SOPEN
Thus, the default size of hdr->CHANNEL
is obtained by calling calloc
with the number of elements set to 0. Technically speaking, the behavior of calloc
in such a scenario is implementation-defined, but on many platforms (including the x86-64 Linux platform used in our testing), this results an allocation of the smallest size possible.
Zooming out quickly reveals the problem: sopen_rhs2000_read
assumes hdr->CHANNEL
is big enough to fit at least one CHANNEL_TYPE
structure (the one for the time channel), even if hdr->CHANNEL
hasn’t been resized since its initial “0” size allocation via constructHDR
.
This can be demonstrated dynamically by supplying the attached POC file to libbiosig and attaching a debugger:
Breakpoint 2, sopen_rhs2000_read (hdr=hdr@entry=0x519000001980) at ./t210/sopen_rhd2000_read.c:464
464 hc->OnOff = 2;
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/t210/sopen_rhd2000_read.c:464
459 NS += NumChans1 - NumChans0;
460 }
461 }
462 { // channel 0 - time channel
463 CHANNEL_TYPE *hc = hdr->CHANNEL+0;
► 464 hc->OnOff = 2;
465 strcpy(hc->Label, "time");
466 strcpy(hc->Transducer, "");
467 #ifdef MAX_LENGTH_PHYSDIM
468 strcpy(hc->PhysDim,"s");
469 #endif
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0 0x7ffff73ceae8 sopen_rhs2000_read+7384
1 0x7ffff730def5 sopen_extended+57349
2 0x5555555554c8 main+511
3 0x7ffff6a2a1ca __libc_start_call_main+122
4 0x7ffff6a2a28b __libc_start_main+139
5 0x555555555205 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x hc
$1 = 0x502000000190
pwndbg> p/x hdr->CHANNEL
$2 = 0x502000000190 // [14]
pwndbg> heap -v 0x502000000180
Allocated chunk | PREV_INUSE
Addr: 0x502000000180
prev_size: 0x00
size: 0x20 (with flag bits: 0x21) // [15]
...
pwndbg> p/x sizeof(CHANNEL_TYPE)
$3 = 0x158 // [16]
Breaking right before the first write to a member of hc
, we can see from the GDB output above that the allocated size of hdr->CHANNEL
is only 16 bytes (GDB shows the size as 0x20, but 16 of these bytes are reserved for heap metadata, making the total usable size 16 bytes) [15]. Given that this is substantially smaller than the size of a single CHANNEL_TYPE
structure (344 bytes [16]), it’s safe to say hdr->CHANNEL
hasn’t been resized since it’s initial 0-sized allocation via constructHDR
.
Some quick tests via GDB reveal that attempting to access any members of the “first” CHANNEL_TYPE
struct besides the first two (PhysMin
and PhysMax
) will result in an out-of-bounds access due to tiny size allocated to hdr->CHANNEL
:
pwndbg> print (int)&((CHANNEL_TYPE*)0)->PhysMin
$4 = 0
pwndbg> print (int)&((CHANNEL_TYPE*)0)->PhysMax
$5 = 8
pwndbg> print (int)&((CHANNEL_TYPE*)0)->DigMin
$6 = 16
Unfortunately, this means that almost every write performed in the time channel code will result in a heap-based buffer overflow, as almost all of these write to a CHANNEL_TYPE
struct member outside the range of allocated memory. The remainder of this analysis will focus on the example encountered when using the attached POC as the input file, located on line 464 of sopen_rhd200_read.c:
{ // channel 0 - time channel
CHANNEL_TYPE *hc = hdr->CHANNEL+0;
hc->OnOff = 2;
The vulnerable line of code attempts to write the value 2 to hdr->CHANNEL[0].OnOff
. Since the OnOff
member is located at an offset of 136 bytes into the CHANNEL_TYPE
struct:
pwndbg> print (int)&((CHANNEL_TYPE*)0)->OnOff
$7 = 136
this write should result in a heap-based buffer overflow. Sure enough, continuing execution from here trips AddressSantizer with a heap-based buffer overflow condition:
pwndbg> c
Continuing.
=================================================================
==82233==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000218 at pc 0x7ffff73cffad bp 0x7fffffffa280 sp 0x7fffffffa270
WRITE of size 1 at 0x502000000218 thread T0 // [17]
#0 0x7ffff73cffac in sopen_rhs2000_read t210/sopen_rhd2000_read.c:464
#1 0x7ffff730def4 in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10758
#2 0x5555555554c7 in main harness.cpp:38
#3 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)
0x502000000218 is located 135 bytes after 1-byte region [0x502000000190,0x502000000191)
allocated by thread T0 here:
#0 0x7ffff78fd340 in calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
#1 0x7ffff72cbf53 in constructHDR /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:1321
#2 0x555555555411 in main harness.cpp:35
#3 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)
SUMMARY: AddressSanitizer: heap-buffer-overflow t210/sopen_rhd2000_read.c:464 in sopen_rhs2000_read
Shadow bytes around the buggy address:
0x501fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502000000000: fa fa 00 00 fa fa 00 fa fa fa 00 fa fa fa fd fa
0x502000000080: fa fa fd fd fa fa fd fd fa fa 00 00 fa fa 00 fa
0x502000000100: fa fa 04 fa fa fa fd fd fa fa 00 00 fa fa 00 fa
0x502000000180: fa fa 01 fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x502000000200: fa fa fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000480: 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
==82233==ABORTING
[Inferior 1 (process 82233) exited with code 01]
pwndbg> p/x 0x502000000218 - 0x502000000190
$3 = 0x88 // [18]
As expected, the overflow occurs on line 464 of sopen_rhd200_read.c, the write is of size 1, and the destination address of the write (0x502000000218) [17] is located exactly 136 (0x88) bytes past the previously computed address of hdr->CHANNEL
(0x502000000190) [14]. For this specific write, the attacker doesn’t have much control of the write destination, as it occurs at a fixed offset from the hdr->CHANNEL
address, nor the data written, as it is always the value 2. This doesn’t hold true for every manifestation of this vulnerability, however, as some write data that is computed based on data from the input file, making it at least partially attacker-controlled. One such example can be found on line 464 of sopen_rhd200_read.c, where the data written is an expression based on the value of hdr->SampleRate
:
{ // channel 0 - time channel
CHANNEL_TYPE *hc = hdr->CHANNEL+0;
...
hc->Cal = 1.0/hdr->SampleRate;
The value of hdr->SampleRate
is read from the input file, making it attacker-controlled, as shown further up in sopen_rhs2000_read
:
int sopen_rhs2000_read(HDRTYPE* hdr) {
...
hdr->SampleRate = lef32p(hdr->AS.Header+8);
Thus, this vulnerability allows for a heap-based buffer overflow where the data written is controlled by the attacker, albeit bounded. Depending on the setup of the heap, this flaw can potentially lead to arbitrary code execution.
==113377==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000218 at pc 0x7ffff73cffad bp 0x7fffffffa250 sp 0x7fffffffa240
WRITE of size 1 at 0x502000000218 thread T0
#0 0x7ffff73cffac in sopen_rhs2000_read t210/sopen_rhd2000_read.c:464
#1 0x7ffff730def4 in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10758
#2 0x5555555554c7 in main harness.cpp:38
#3 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)
0x502000000218 is located 135 bytes after 1-byte region [0x502000000190,0x502000000191)
allocated by thread T0 here:
#0 0x7ffff78fd340 in calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
#1 0x7ffff72cbf53 in constructHDR /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:1321
#2 0x555555555411 in main harness.cpp:35
#3 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)
SUMMARY: AddressSanitizer: heap-buffer-overflow t210/sopen_rhd2000_read.c:464 in sopen_rhs2000_read
Shadow bytes around the buggy address:
0x501fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502000000000: fa fa 00 00 fa fa 00 fa fa fa 00 fa fa fa fd fa
0x502000000080: fa fa fd fd fa fa fd fd fa fa 00 00 fa fa 00 fa
0x502000000100: fa fa 04 fa fa fa fd fd fa fa 00 00 fa fa 00 fa
0x502000000180: fa fa 01 fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x502000000200: fa fa fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000480: 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
==113377==ABORTING
[Inferior 1 (process 113377) exited with code 01]
2025-08-06 - Vendor Disclosure
2025-08-24 - Vendor Patch Release
2025-08-25 - Public Release
Discovered by Mark Bereza and Lilith >_> of Cisco Talos.