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

Fix memory corruption #25

Closed
wants to merge 1 commit into from
Closed

Fix memory corruption #25

wants to merge 1 commit into from

Conversation

gavv
Copy link
Contributor

@gavv gavv commented Nov 12, 2019

Hi and thanks for sharing this library.

Currently, all decode methods have two problems:

  1. They pass to opus_decode() the total number of samples in output buffer. But according to documentation, they should pass number of samples per channel. In result, in two-channel mode opus will think that the buffer is twice larger than it is. This may lead to memory corruption which is actually reproducing in my application. It's not reproduced each time. It seems that opus usually determines the frame size from its contents and doesn't touch the memory beyond that size.

  2. They use cap() instead of len(). I believe this is a bad decision for API because if I pass data[:n] to a function, I expect that the memory beyond n will not be touched. But with current API it may happen if the slice capacity is larger than the slice length. Such bugs may be very hard to find, especially if your slice comes from some other component and refers to a part of a larger memory region (e.g. in pool).

This patch fixes both issues. If you don't agree with the second point, which may be arguable, I can revert the second part of the fix and use cap() instead of len().

I've also added a test. This test fails before applying the patch and succeeds after applying it.

@hraban
Copy link
Owner

hraban commented Nov 12, 2019

Ugh. How on earth is this possible. I spent a million years looking at these docs and here we are.

Thanks for the patch. I'll have a look over tonight, but it looks like you're right.

Please also add yourself to the AUTHORS list.

@gavv
Copy link
Contributor Author

gavv commented Nov 12, 2019

Please also add yourself to the AUTHORS list.

Done.

@ydnar
Copy link
Contributor

ydnar commented Nov 13, 2019

Yikes. Thanks @gavv!

@hraban
Copy link
Owner

hraban commented Nov 14, 2019

Hi gavv I don't have time to look at the cap vs len thing for at least another week. I like the idea but I need to think through the possible implications for people already depending on this code.

In the mean time, especially now that this bug is out in the open, I'd like to merge the memory issue. Would you mean removing the len/cap code from this PR and creating another one?

Thanks!

Copy link
Owner

@hraban hraban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! 👍

decoder.go Outdated
@@ -74,12 +76,15 @@ func (dec *Decoder) Decode(data []byte, pcm []int16) (int, error) {
if len(pcm) == 0 {
return 0, fmt.Errorf("opus: target buffer empty")
}
if len(pcm)%dec.channels != 0 {
return 0, fmt.Errorf("opus: output buffer length must be multiple of channels")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return 0, fmt.Errorf("opus: output buffer length must be multiple of channels")
return 0, fmt.Errorf("opus: target buffer length must be multiple of channels")

decoder.go Outdated
@@ -99,12 +104,15 @@ func (dec *Decoder) DecodeFloat32(data []byte, pcm []float32) (int, error) {
if len(pcm) == 0 {
return 0, fmt.Errorf("opus: target buffer empty")
}
if len(pcm)%dec.channels != 0 {
return 0, fmt.Errorf("opus: output buffer length must be multiple of channels")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return 0, fmt.Errorf("opus: output buffer length must be multiple of channels")
return 0, fmt.Errorf("opus: target buffer length must be multiple of channels")

decoder.go Outdated
@@ -125,13 +133,15 @@ func (dec *Decoder) DecodeFEC(data []byte, pcm []int16) error {
if len(pcm) == 0 {
return fmt.Errorf("opus: target buffer empty")
}

if len(pcm)%dec.channels != 0 {
return fmt.Errorf("opus: output buffer length must be multiple of channels")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("opus: output buffer length must be multiple of channels")
return fmt.Errorf("opus: target buffer length must be multiple of channels")

decoder.go Outdated
@@ -152,12 +162,15 @@ func (dec *Decoder) DecodeFECFloat32(data []byte, pcm []float32) error {
if len(pcm) == 0 {
return fmt.Errorf("opus: target buffer empty")
}
if len(pcm)%dec.channels != 0 {
return fmt.Errorf("opus: output buffer length must be multiple of channels")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("opus: output buffer length must be multiple of channels")
return fmt.Errorf("opus: target buffer length must be multiple of channels")

t.Run("larger-buffer-float32-fec", func(t *testing.T) {
decodeFecFloat32(t, encodeFrame(t), FRAME_SIZE+1, false)
})
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*sigh, go and generics...

decoder.go Outdated
n := int(C.opus_decode(
dec.p,
(*C.uchar)(&data[0]),
C.opus_int32(len(data)),
(*C.opus_int16)(&pcm[0]),
C.int(cap(pcm)),
C.int(len(pcm)/dec.channels),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
C.int(len(pcm)/dec.channels),
C.int(cap(pcm)/dec.channels),

sticking to cap for this PR, will think on cap/len later.

decoder.go Outdated
n := int(C.opus_decode_float(
dec.p,
(*C.uchar)(&data[0]),
C.opus_int32(len(data)),
(*C.float)(&pcm[0]),
C.int(cap(pcm)),
C.int(len(pcm)/dec.channels),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
C.int(len(pcm)/dec.channels),
C.int(cap(pcm)/dec.channels),

decoder.go Outdated
n := int(C.opus_decode(
dec.p,
(*C.uchar)(&data[0]),
C.opus_int32(len(data)),
(*C.opus_int16)(&pcm[0]),
C.int(cap(pcm)),
C.int(len(pcm)/dec.channels),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
C.int(len(pcm)/dec.channels),
C.int(cap(pcm)/dec.channels),

decoder.go Outdated
n := int(C.opus_decode_float(
dec.p,
(*C.uchar)(&data[0]),
C.opus_int32(len(data)),
(*C.float)(&pcm[0]),
C.int(cap(pcm)),
C.int(len(pcm)/dec.channels),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
C.int(len(pcm)/dec.channels),
C.int(cap(pcm)/dec.channels),

@hraban
Copy link
Owner

hraban commented Nov 14, 2019

Forget about the github UI, I've done it old school locally and just pushed it. Your patch has been included in v2: cb9658a -- not included just yet, can't make tests work locally. out of time again so will have to look at this... at some point in my life.

@gavv
Copy link
Contributor Author

gavv commented Nov 14, 2019

I like the idea but I need to think through the possible implications for people already depending on this code.

I'd suggest you to make a v3 release. This patch, even with cap() instead of len(), breaks the API anyway.

Would you mean removing the len/cap code from this PR and creating another one?

Sure.

@gavv
Copy link
Contributor Author

gavv commented Nov 14, 2019

I've revered cap/len change, applied your suggestions, and fixed tests.

@hraban
Copy link
Owner

hraban commented Nov 14, 2019

I will have to backport this to v2 and v1 because it's a security issue (for future reference, by the way: if you find memory corruption bug, I recommend reporting it in private at least initially, because putting it out in the open significantly increases the pressure to fix it asap :/ it's not ideal timing at the mo 😭).

@hraban
Copy link
Owner

hraban commented Nov 14, 2019

I'd suggest you to make a v3 release. This patch, even with cap() instead of len(), breaks the API anyway.

Isn't the error in our invocation of libopus? as far as I can see, fixing this should work backwards compatibly for everyone. I was calling libopus wrong, but users of this lib just pass in an array with a cap, no further updates needed (cue also the other tests which still work fine).

Any specific reason you were thinking of a v3?

@gavv
Copy link
Contributor Author

gavv commented Nov 14, 2019

You're right. I was thinking about the behavior change. There are three cases:

  1. user passes a buffer smaller than the actual frame
  2. user passes a buffer of the exact same size
  3. user passes a larger buffer

Old behavior:

  1. corrupted memory 2. usually correct 3. usually correct

New behavior:

  1. error 2. correct 3. correct

So we're changing a (probably silent) memory corruption to an error. I guess this should be considered as a compatible change :)

@gavv gavv closed this Nov 17, 2019
@gavv gavv mentioned this pull request Nov 17, 2019
@gavv
Copy link
Contributor Author

gavv commented Nov 17, 2019

I'd like to push more commits into v2 branch of my fork, which are not related to this PR. Github does not allow me to switch PR's branch, so I'm reopening it with another branch: #26.

I've also replaced len() with cap() in new buffer size checks. I didn't touch the old checks.

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 this pull request may close these issues.

None yet

3 participants