Talos Vulnerability Report

TALOS-2025-2240

The Biosig Project libbiosig RHS2000 parsing heap-based buffer overflow vulnerability

August 25, 2025
CVE Number

CVE-2025-48005

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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)

PRODUCT URLS

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

CVSSv3 SCORE

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-122 - Heap-based Buffer Overflow

DETAILS

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.

Crash Information

==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]
TIMELINE

2025-08-06 - Vendor Disclosure
2025-08-24 - Vendor Patch Release
2025-08-25 - Public Release

Credit

Discovered by Mark Bereza and Lilith >_> of Cisco Talos.