Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STM loader should ignore volume=0 samples for certain .STMs. #6

Closed
AliceLR opened this issue Oct 13, 2020 · 7 comments · Fixed by #13
Closed

STM loader should ignore volume=0 samples for certain .STMs. #6

AliceLR opened this issue Oct 13, 2020 · 7 comments · Fixed by #13

Comments

@AliceLR
Copy link
Collaborator

AliceLR commented Oct 13, 2020

Certain .STM files rely on dummying out samples with 0 volume. There's a check for this in load_stm.c, but it's commented out. I'm not sure why this is the case, but here's a .STM from Mod Archive that definitely relies on this behavior to work correctly.

georythm.stm.zip

Other players' behaviors: libxmp has the same bug but also allows the mod to load, resulting in corrupted samples that sound awful. libmodplug unconditionally dummies these samples out and the file plays correctly. libopenmpt plays the file correctly and does effectively the same check, but in a different location.

@sezero
Copy link
Owner

sezero commented Oct 13, 2020

Certain .STM files rely on dummying out samples with 0 volume. There's a check for this in load_stm.c, but it's commented out. I'm not sure why this is the case

The only reference I found about load_stm.c history is this:
https://github.com/sezero/mikmod/blob/master/libmikmod/NEWS#L842
Seems to have happened between mikmod-3.1.2 and libmikmod-3.1.5.

@AliceLR
Copy link
Collaborator Author

AliceLR commented Oct 13, 2020

I checked those versions and that does appear to be when that was changed:
stm_3.1.2_to_3.1.5.diff.txt

There's a second bug that the libxmp PR linked above also fixes, which is the sample positions are determined by the sample reserved field *16. For whatever reason the volume=0 hack alone seems to break fracture.stm and this fix is required to get the fracture.stm samples to load correctly. (edit: both libmodplug and libopenmpt also have this fix.)

Here's fracture.stm (also available in the libxmp repo) for convenience.
fracture.stm.zip

Working patch that allows MikMod to load and play both georythm.stm and fracture.stm correctly. I think the MOD2STM files and mysterious "some STM modules" that didn't have their samples loaded correctly deserve further investigation (though not using the reserved field might explain the latter).

diff --git a/libmikmod/loaders/load_stm.c b/libmikmod/loaders/load_stm.c
index 406bea8..c97dda9 100644
--- a/libmikmod/loaders/load_stm.c
+++ b/libmikmod/loaders/load_stm.c
@@ -255,7 +255,6 @@ static BOOL STM_LoadPatterns(void)
 static BOOL STM_Load(BOOL curious)
 {
 	int t;
-	ULONG MikMod_ISA; /* We must generate our own ISA, it's not stored in stm */
 	SAMPLE *q;
 
 	/* try to read stm header */
@@ -331,8 +330,6 @@ static BOOL STM_Load(BOOL curious)
 
 	if(!AllocSamples()) return 0;
 	if(!STM_LoadPatterns()) return 0;
-	MikMod_ISA=_mm_ftell(modreader);
-	MikMod_ISA=(MikMod_ISA+15)&0xfffffff0;	/* normalize */
 
 	for(q=of.samples,t=0;t<of.numsmp;t++,q++) {
 		/* load sample info */
@@ -340,13 +337,10 @@ static BOOL STM_Load(BOOL curious)
 		q->speed      = (mh->sample[t].c2spd * 8363) / 8448;
 		q->volume     = mh->sample[t].volume;
 		q->length     = mh->sample[t].length;
-		if (/*!mh->sample[t].volume || */q->length==1) q->length=0;
+		if (!mh->sample[t].volume || q->length==1) q->length=0;
 		q->loopstart  = mh->sample[t].loopbeg;
 		q->loopend    = mh->sample[t].loopend;
-		q->seekpos    = MikMod_ISA;
-
-		MikMod_ISA+=q->length;
-		MikMod_ISA=(MikMod_ISA+15)&0xfffffff0;	/* normalize */
+		q->seekpos    = mh->sample[t].reserved << 4;
 
 		/* contrary to the STM specs, sample data is signed */
 		q->flags = SF_SIGNED;

edit: from the same diff I uploaded above, the MOD2STM comment in the changelog might just be a reference to adding support for the BMOD2STM signature.

@sezero
Copy link
Owner

sezero commented Oct 13, 2020

Only briefly read your patch, yet. Is it well-tested anything other than those you noted above?

@sezero
Copy link
Owner

sezero commented Oct 13, 2020

Note: q->seekpos = mh->sample[t].reserved << 4; might need validating against file length (or position?) to guard against malicious files?

@AliceLR
Copy link
Collaborator Author

AliceLR commented Oct 13, 2020

So far I've only tested that patch against a small handful of files so it definitely needs more testing.

Note: q->seekpos = mh->sample[t].reserved << 4; might need validating against file length (or position?) to guard against malicious files?

Here's an updated patch that adds a bounds check. (edit: the check is specifically limited to q->length being set since otherwise this was incorrectly flagging some dummied samples)

diff --git a/libmikmod/loaders/load_stm.c b/libmikmod/loaders/load_stm.c
index 406bea8..5e402d0 100644
--- a/libmikmod/loaders/load_stm.c
+++ b/libmikmod/loaders/load_stm.c
@@ -255,7 +255,8 @@ static BOOL STM_LoadPatterns(void)
 static BOOL STM_Load(BOOL curious)
 {
 	int t;
-	ULONG MikMod_ISA; /* We must generate our own ISA, it's not stored in stm */
+	ULONG samplestart;
+	ULONG sampleend;
 	SAMPLE *q;
 
 	/* try to read stm header */
@@ -331,8 +332,10 @@ static BOOL STM_Load(BOOL curious)
 
 	if(!AllocSamples()) return 0;
 	if(!STM_LoadPatterns()) return 0;
-	MikMod_ISA=_mm_ftell(modreader);
-	MikMod_ISA=(MikMod_ISA+15)&0xfffffff0;	/* normalize */
+
+	samplestart=_mm_ftell(modreader);
+	_mm_fseek(modreader,0,SEEK_END);
+	sampleend=_mm_ftell(modreader);
 
 	for(q=of.samples,t=0;t<of.numsmp;t++,q++) {
 		/* load sample info */
@@ -340,13 +343,16 @@ static BOOL STM_Load(BOOL curious)
 		q->speed      = (mh->sample[t].c2spd * 8363) / 8448;
 		q->volume     = mh->sample[t].volume;
 		q->length     = mh->sample[t].length;
-		if (/*!mh->sample[t].volume || */q->length==1) q->length=0;
+		if (!mh->sample[t].volume || q->length==1) q->length=0;
 		q->loopstart  = mh->sample[t].loopbeg;
 		q->loopend    = mh->sample[t].loopend;
-		q->seekpos    = MikMod_ISA;
+		q->seekpos    = mh->sample[t].reserved << 4;
 
-		MikMod_ISA+=q->length;
-		MikMod_ISA=(MikMod_ISA+15)&0xfffffff0;	/* normalize */
+		/* Sanity check to make sure samples are bounded within the file. */
+		if(q->length && (q->seekpos<samplestart || (q->seekpos+q->length)>sampleend)) {
+			_mm_errno = MMERR_LOADING_SAMPLEINFO;
+			return 0;
+		}
 
 		/* contrary to the STM specs, sample data is signed */
 		q->flags = SF_SIGNED;

@AliceLR
Copy link
Collaborator Author

AliceLR commented Oct 14, 2020

I've done a bit more testing on this patch. I found two unrelated bugs that I can diagnose and report as a separate issue. As far as this patch goes, the sample bounds check is maybe too aggressive but otherwise every .STM I throw at MikMod with this patch loads and has correct samples.

[1]: every STM loaded seems to have a buggy extra order appended to the end of the orders list (off-by-one?). Order number is noted for most occurences.
[2]: unused patterns between 0 and 63 are treated by ST 2.3 as valid blank patterns. MikMod seems to remove these (independently of the bug above). These are typically at the end of the .STM to add a break or to allow samples to decay. edit: minor correction, this implied before that 64-98 are valid pattern numbers and they are not.

works=indistinguishable from ST 2.3 (DOSBox, SB16, 25k) playback as far as I can tell.

File MikMod 3.3.11 libxmp (master) MikMod (patch)
axel_f.stm works works works (unchanged)
fracture.stm works ([1]: order 57) loads; corrupted samples works (unchanged)
georythm.stm doesn't load loads; corrupted samples works ([1]: order 12)
mission1.stm from libxmp/libxmp#92 doesn't load loads; corrupted samples works ([1]: order 36)
wonder.stm from libxmp/libxmp#92 doesn't load loads; corrupted samples works [1][2]

I also went though every .STM on modland.com (since there's a reasonably low number of them) and tested every track that MikMod 3.3.11 fails to load. It might be worth allowing some degree of sample truncation since a couple of these seem to rely on it to load.

File libxmp (master) MikMod (patch)
andrzej s.1.stm loads; corrupted samples works [1][2]
jam session.stm loads; corrupted samples? rejected sample # 6 (samplestart=14480, sampleend=69376, seekpos=60112, length=9447)
mayhem #1 main.stm works? rejected sample # 13 (samplestart=21648, sampleend=168000, seekpos=166112, length=2103)
scorp.stm loads; corrupted samples works ([1]: order 29)
soul.stm works works ([1]: order 11)
aye bee sees.stm works works ([1]: order 39)
eurolight orchestra.stm doesn't load in ST 2.3 ;-( works works ([1]: order 39)
depesh mode 1.stm [sic] works works ([1]: order 19)
emf - intro.stm loads; corrupted samples works ([1]: order 22)
fordemo1.stm loads; corrupted samples works ([1]: order 29)
fordemo3.stm loads; corrupted samples works ([1]: order 20)
hava nagila.stm works works ([1]: order 18)
acid lambada.stm works? works ([1]: order 23)
blow it up!.stm loads; corrupted samples works ([1]: order 13)
indian warrior.stm loads; corrupted samples works ([1]: order 28)
the universal ones (a space age melody).stm works? works [1][2]

@AliceLR
Copy link
Collaborator Author

AliceLR commented Oct 14, 2020

Here's an updated patch that allows sample truncation for the two truncated .STMs in the previous comment. Not really sure about setting seekpos to 0 though.

diff --git a/libmikmod/loaders/load_stm.c b/libmikmod/loaders/load_stm.c
index 406bea8..b4772dc 100644
--- a/libmikmod/loaders/load_stm.c
+++ b/libmikmod/loaders/load_stm.c
@@ -255,7 +255,8 @@ static BOOL STM_LoadPatterns(void)
 static BOOL STM_Load(BOOL curious)
 {
 	int t;
-	ULONG MikMod_ISA; /* We must generate our own ISA, it's not stored in stm */
+	ULONG samplestart;
+	ULONG sampleend;
 	SAMPLE *q;
 
 	/* try to read stm header */
@@ -331,8 +332,10 @@ static BOOL STM_Load(BOOL curious)
 
 	if(!AllocSamples()) return 0;
 	if(!STM_LoadPatterns()) return 0;
-	MikMod_ISA=_mm_ftell(modreader);
-	MikMod_ISA=(MikMod_ISA+15)&0xfffffff0;	/* normalize */
+
+	samplestart=_mm_ftell(modreader);
+	_mm_fseek(modreader,0,SEEK_END);
+	sampleend=_mm_ftell(modreader);
 
 	for(q=of.samples,t=0;t<of.numsmp;t++,q++) {
 		/* load sample info */
@@ -340,13 +343,35 @@ static BOOL STM_Load(BOOL curious)
 		q->speed      = (mh->sample[t].c2spd * 8363) / 8448;
 		q->volume     = mh->sample[t].volume;
 		q->length     = mh->sample[t].length;
-		if (/*!mh->sample[t].volume || */q->length==1) q->length=0;
+		if (!mh->sample[t].volume || q->length==1) q->length=0;
 		q->loopstart  = mh->sample[t].loopbeg;
 		q->loopend    = mh->sample[t].loopend;
-		q->seekpos    = MikMod_ISA;
+		q->seekpos    = mh->sample[t].reserved << 4;
 
-		MikMod_ISA+=q->length;
-		MikMod_ISA=(MikMod_ISA+15)&0xfffffff0;	/* normalize */
+		/* Sanity checks to make sure samples are bounded within the file. */
+		if(q->length) {
+			if(q->seekpos<samplestart) {
+#ifdef MIKMOD_DEBUG
+				fprintf(stderr,"rejected sample # %d (seekpos=%u < samplestart=%u)\n",t,q->seekpos,samplestart);
+#endif
+				_mm_errno = MMERR_LOADING_SAMPLEINFO;
+				return 0;
+			}
+			/* Some .STMs seem to rely on allowing truncated samples... */
+			if(q->seekpos>=sampleend) {
+#ifdef MIKMOD_DEBUG
+				fprintf(stderr,"truncating sample # %d from length %u to 0\n",t,q->length);
+#endif
+				q->seekpos = q->length = 0;
+			} else if(q->seekpos+q->length>sampleend) {
+#ifdef MIKMOD_DEBUG
+				fprintf(stderr,"truncating sample # %d from length %u to %u\n",t,q->length,sampleend - q->seekpos);
+#endif
+				q->length = sampleend - q->seekpos;
+			}
+		}
+		else
+			q->seekpos = 0;
 
 		/* contrary to the STM specs, sample data is signed */
 		q->flags = SF_SIGNED;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants