Talos Vulnerability Report

TALOS-2025-2237

The Biosig Project libbiosig MFER default NS mismatch heap-based buffer overflow vulnerability

August 25, 2025
CVE Number

CVE-2025-53511

SUMMARY

A heap-based buffer overflow vulnerability exists in the MFER parsing functionality of The Biosig Project libbiosig 3.9.0 and Master Branch (35a819fa). A specially crafted MFER 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 strctures, 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 Medical waveform Format Encoding Rules (MFER), a file format for encoding medical waveforms from various different kinds of medical devices, such as ECG and EEG. The MFER standard aims to abstract away the encoding of the waveform data itself, independent of the specifics of the recording device.

To determine if the input file is valid MFER, getfiletype runs the following check:

		else if (!memcmp(Header1,"@  MFER ",8))
			hdr->TYPE = MFER;
		else if (!memcmp(Header1,"@ MFR ",6))
			hdr->TYPE = MFER;

Put simply, libbiosig classifies an input file as MFER if the first few bytes match one of two magic byte sequences 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 MFER, processing unique to the format is then performed. This code block starts by defining some temporary variables that will be used for parsing the MFER file, including initializing the number of channels (hdr->NS) to 1 [1], which will become important later. From there, the bulk of the file processing is done inside a while loop [2] that continues so long as there are remaining bytes to be read from the input file, which hdr is a handle to:

		else if (hdr->TYPE==MFER) {

		...

			uint8_t buf[128];
		void* ptrbuf = buf;
		uint8_t gdftyp = 3; 	// default: int16
		uint8_t UnitCode=0;
		double Cal = 1.0, Off = 0.0;
		char SWAP = ( __BYTE_ORDER == __LITTLE_ENDIAN);   // default of MFER is BigEndian
		hdr->FILE.LittleEndian = 0;
		hdr->SampleRate = 1000;	// default sampling rate is 1000 Hz
		hdr->NS = 1;			// default number of channels is 1                                                  // [1]
		/* TAG */                                                                                                   // [3]
		uint8_t tag = hdr->AS.Header[0];
			ifseek(hdr,1,SEEK_SET);
			int curPos = 1;
		size_t N_EVENT=0;	// number of events, memory is allocated for in the event table.
		while (!ifeof(hdr)) {                                                                                       // [2]
			uint32_t len, val32=0;
			int32_t  chan=-1;
			uint8_t tmplen;

			if (tag==255)
				break;
			else if (tag==63) {
				/* CONTEXT */
				curPos += ifread(buf,1,1,hdr);
				chan = buf[0] & 0x7f;
				while (buf[0] & 0x80) {
					curPos += ifread(buf,1,1,hdr);
					chan    = (chan<<7) + (buf[0] & 0x7f);
				}
			}

			/* LENGTH */                                                                                            // [4]
			curPos += ifread(&tmplen,1,1,hdr);
			char FlagInfiniteLength = 0;
			if ((tag==63) && (tmplen==0x80)) {
				FlagInfiniteLength = -1; //Infinite Length
				len = 0;
			}
			else if (tmplen & 0x80) {
				tmplen &= 0x7f;
				curPos += ifread(&buf,1,tmplen,hdr);
				len = 0;
				k = 0;
				while (k<tmplen)
					len = (len<<8) + buf[k++];
			}
			else
				len = tmplen;

			if (VERBOSE_LEVEL>7)
				fprintf(stdout,"MFER: tag=%3i chan=%2i len=%i %3i curPos=%i %li\n",tag,chan,tmplen,len,curPos,iftell(hdr));

			/* VALUE */                                                                                             // [5]
			if (tag==0) {
				if (len!=1) fprintf(stderr,"Warning MFER tag0 incorrect length %i!=1\n",len);
				curPos += ifread(buf,1,len,hdr);
			}
			else if (tag==1) {

			...

The encoding rules for MFER, as described in ISO-22066-1-2022, specify data should be structured in the form Tag, Data Length, Value, or TLV. Broadly speaking, the Tag portion classifies the data, the Data Length portion encodes the length of the data in octets (bytes), and the Value portion encodes the actual payload. This is reflected in the MFER processing code above, with the relevant blocks for parsing the Tag [3], Length [4], and Value [5] fields annotated appropriately.

The primary job of the TAG block is simply to extract the tag from the first octet of the header, which is then stored in the tag variable. From there, the LENGTH block reads the length from the input file into len, which can be encoded as a single octet, multiple octets, or an “indefinite data length” flag, where the data is instead terminated via a special “end-of-contents” frame. Finally, the bulk of the processing is performed in the VALUE block. The meaning of the data is dependent on the tag, so this code largely consists of numerous conditional statements that perform unique processing depending on the value of tag. That said, this vulnerability is exercised in the code responsbile for processing channel data, which occurs after the aforementioned while loop has completed and all the frames have been parsed:

		hdr->AS.bpb = 0;
		for (k=0; k<hdr->NS; k++) {                                                                                 // [6]

			if (VERBOSE_LEVEL>8)
				fprintf(stdout,"sopen(MFER): #%i\n",(int)k);

			CHANNEL_TYPE *hc = hdr->CHANNEL+k;                                                                      // [7]
			hc->Transducer[0] = 0;
			if (!hc->PhysDimCode) hc->PhysDimCode = MFER_PhysDimCodeTable[UnitCode];
			if (hc->Cal==1.0) hc->Cal = Cal;
			hc->Off = Off * hc->Cal;
			if (!hc->SPR) hc->SPR = hdr->SPR;
			if (hc->GDFTYP<16)
				if (hc->GDFTYP & 0x01) {
					hc->DigMax = ldexp( 1.0,GDFTYP_BITS[gdftyp]-1) - 1.0;
					hc->DigMin = ldexp(-1.0,GDFTYP_BITS[gdftyp]-1);
				}
				else {
					hc->DigMax = ldexp( 1.0,GDFTYP_BITS[gdftyp]);
					hc->DigMin = 0.0;
				}
			else {
				hc->DigMax =  INFINITY;
				hc->DigMin = -INFINITY;
			}
			hc->PhysMax = hc->DigMax * hc->Cal + hc->Off;
			hc->PhysMin = hc->DigMin * hc->Cal + hc->Off;
				hc->OnOff   = 1;
				hc->bi      = hdr->AS.bpb;
			hdr->AS.bpb += hdr->SPR*(GDFTYP_BITS[gdftyp]>>3);
		}

The channel processing code at the tail end of the MFER processing block is done via a for loop [6] that iterates over each channel one by one, with k holding the index of the current channel being processed and the total number of channels (hdr->NS) used as the loop’s upper bound. For context, libbiosig stores channel data in hdr->CHANNEL, a heap allocated array of CHANNEL_TYPE structures - one per channel. 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;

With that context established, we can see that the channel processing for loop begins by saving a pointer to the current CHANNEL_TYPE structure being processed to the local variable hc [7], which is then used as a handle to write to various members of that struct. Unfortunately, this leads to a heap-based buffer overflow vulnerability due to a mismatch between the initial size allocated for the hdr->CHANNEL array and the default value of hdr->NS in the MFER processing code.

As mentioned previously, hdr->NS is initialized to 1 at the start of the MFER processing code and, within this scope, is only changed from this default value when parsing a frame using tag 5:

			else if (tag==5)     //0x05: number of channels
			{
				uint16_t oldNS=hdr->NS;
				if (len>4) fprintf(stderr,"Warning MFER tag5 incorrect length %i>4\n",len);
				curPos += ifread(buf,1,len,hdr);
				hdr->NS = *(int64_t*) mfer_swap8b(buf, len, SWAP);
if (VERBOSE_LEVEL>7) fprintf(stdout,"MFER: TLV %i %i %i \n",tag,len,(int)hdr->NS);
				hdr->CHANNEL = (CHANNEL_TYPE*)realloc(hdr->CHANNEL, hdr->NS*sizeof(CHANNEL_TYPE));                  // [8]

Based on the call to realloc in the code block above [8], libbiosig attempts to keep the size of hdr->CHANNEL synchronized with hdr->NS, which makes sense since hdr->NS stores the number of channels and hdr->CHANNEL stores information for each channel. If this code is never reached during the processing of an MFER file, however, both hdr->NS and the size of hdr->CHANNEL will remain at their default values. We already know that hdr->NS is initialized to 1, but what about the initial size of the hdr->CHANNEL array? This array, along with the majority of the fields for hdr, are initialized via the function constructHDR:

/****************************************************************************/
/**                     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;                                                                                                   // [10]

	...

		// define variable header
	hdr->CHANNEL = (CHANNEL_TYPE*)calloc(hdr->NS, sizeof(CHANNEL_TYPE));                                            // [9]

	...

	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 [9], 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 [10], 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 and comparing the default state for hdr->NS and hdr->CHANNEL quickly reveals the problem: the MFER processing code will assume at least 1 channel is present (and thus at least one entry in the hdr->CHANNEL array) 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_extended (FileName=<optimized out>, MODE=<optimized out>, hdr=0x519000001980, biosig_options=<optimized out>)
	at biosig.c:9246
9246                    for (k=0; k<hdr->NS; k++) {
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:9246
9241                         int sz=ifread(&tag,1,1,hdr);
9242                         curPos += sz;
9243                  }
9244                 hdr->FLAG.OVERFLOWDETECTION = 0;         // overflow detection OFF - not supported
9245                  hdr->AS.bpb = 0;
► 9246                  for (k=0; k<hdr->NS; k++) {
9247
9248                          if (VERBOSE_LEVEL>8)
9249                                  fprintf(stdout,"sopen(MFER): #%i\n",(int)k);
9250
9251                         CHANNEL_TYPE *hc = hdr->CHANNEL+k;
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff73339a5 sopen_extended+211637
1   0x5555555554c8 main+511
2   0x7ffff6a2a1ca __libc_start_call_main+122
3   0x7ffff6a2a28b __libc_start_main+139
4   0x555555555205 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x hdr->NS
$1 = 0x1
pwndbg> p/x hdr->CHANNEL
$2 = 0x502000000190																									// [11]
pwndbg> heap -v 0x502000000180
Allocated chunk | PREV_INUSE
Addr: 0x502000000180
prev_size: 0x00
size: 0x20 (with flag bits: 0x21)                                                                                   // [12]

...

pwndbg> p/x sizeof(CHANNEL_TYPE)
$3 = 0x158                                                                                                          // [13]

Breaking right before entering the for loop that performs the channel processing, we can see from the GDB output above that hdr->NS is still at its default value of 1. Meanwhile, hdr->CHANNEL is located at the address 0x502000000190 [11] as has an allocated size of 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) [12]. Given that this is substantially smaller than the size of a single CHANNEL_TYPE structure (344 bytes) [13], 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 the tiny size allocated for 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 body of the channel processing for loop will result in a heap-based buffer overflow condition, 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 first example encountered, located on line 9252 of biosig.c:

 	for (k=0; k<hdr->NS; k++) {

 		...

		CHANNEL_TYPE *hc = hdr->CHANNEL+k;
		hc->Transducer[0] = 0;

Since we’ve already established that hdr->NS, the upper bound for the for loop, is 1, we know that hc is equal to hdr->CHANNEL[0]. Thus, the vulnerable line of code attempts to write a null byte to hdr->CHANNEL[0].Transducer[0]. Since the Transducer member is located at an offset of 152 bytes into the CHANNEL_TYPE struct:

pwndbg> print (int)&((CHANNEL_TYPE*)0)->Transducer
$7 = 152

the writing of this null byte 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.
=================================================================
==49789==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000228 at pc 0x7ffff734fa41 bp 0x7fffffffa7d0 sp 0x7fffffffa7c0
WRITE of size 1 at 0x502000000228 thread T0
	#0 0x7ffff734fa40 in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:9252
	#1 0x5555555554c7 in main harness.cpp:38
	#2 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
	#3 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
	#4 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)

0x502000000228 is located 151 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 /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:9252 in sopen_extended
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
==49789==ABORTING
[Inferior 1 (process 49789) exited with code 01]
pwndbg> p/x 0x502000000228 - 0x502000000190
$8 = 0x98                                                                                                           // [14]

As expected, the overflow occurs on line 9252 of biosig.c, the write is of size 1, and the destination address of the write (0x502000000228) is located exactly 152 (0x98) bytes past the previously computed address of hdr->CHANNEL (0x502000000190) [14]. With this specific POC, 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 a single null byte. 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 occurs on line 9256 of biosig.c:

		for (k=0; k<hdr->NS; k++) {

			...

			CHANNEL_TYPE *hc = hdr->CHANNEL+k;

			...

			if (!hc->SPR) hc->SPR = hdr->SPR;

This results in a write to the SPR member of the CHANNEL_TYPE struct if the current value is 0. The data written is the value of hdr->SPR, which can be set using data from the input file by sending a frame with tag 4:

			else if (tag==4) {
				// SPR
				if (len>4) fprintf(stderr,"Warning MFER tag4 incorrect length %i>4\n",len);
				curPos += ifread(buf,1,len,hdr);
				hdr->SPR = *(int64_t*) mfer_swap8b(buf, len, SWAP);

Thus, 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.

Crash Information

==49789==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000228 at pc 0x7ffff734fa41 bp 0x7fffffffa7d0 sp 0x7fffffffa7c0
WRITE of size 1 at 0x502000000228 thread T0
	#0 0x7ffff734fa40 in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:9252
	#1 0x5555555554c7 in main harness.cpp:38
	#2 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
	#3 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
	#4 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)

0x502000000228 is located 151 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 /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:9252 in sopen_extended
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
==49789==ABORTING
[Inferior 1 (process 49789) 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.