From e339caa5b47276a58ed54ddb7e0b303225fb9d58 Mon Sep 17 00:00:00 2001 From: Aizal Khan Date: Wed, 24 Jun 2026 22:03:23 +0530 Subject: [PATCH 1/2] fix use-after-free in WavpackUnpackSamples after a failed seek A failed WavpackSeekSample64() frees the stream's raw block buffer but leaves wphdr describing a valid block, so the next decode read from freed memory. Treat a NULL blockbuff as no current block so a fresh block is read instead. --- fuzzing/fuzzer.cc | 13 +++++++++++++ .../seek-failure-decode-use-after-free.wv | Bin 0 -> 886 bytes src/unpack_utils.c | 5 +++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 fuzzing/regression/seek-failure-decode-use-after-free.wv diff --git a/fuzzing/fuzzer.cc b/fuzzing/fuzzer.cc index 63aa1495..991b625e 100644 --- a/fuzzing/fuzzer.cc +++ b/fuzzing/fuzzer.cc @@ -258,6 +258,19 @@ int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) ++tag_writes; } + // decode again from wherever the seek left us; this must stay safe even when the seek + // above failed (a failed seek must not leave the decoder pointing at freed block data) + + if (num_chans && num_chans <= 256) { + int32_t decoded_samples [BUF_SAMPLES * num_chans]; + int unpack_result; + + do { + unpack_result = WavpackUnpackSamples (wpc, decoded_samples, BUF_SAMPLES); + samples_decoded += unpack_result; + } while (unpack_result); + } + WavpackCloseFile (wpc); exit: diff --git a/fuzzing/regression/seek-failure-decode-use-after-free.wv b/fuzzing/regression/seek-failure-decode-use-after-free.wv new file mode 100644 index 0000000000000000000000000000000000000000..d9e0d4845794d132b3f059a197a6547376863dcc GIT binary patch literal 886 zcmXRfE6C1cW?&FtVPJ4s!vF?585tN1C3lEypU(0>gQ+~Rj6qQ&$kWX&Vj~knxMP@W zR%u?g3`8kTGA*}6K>#Sr$iM_PR{*G&g+YKJC9xzC#Aad&5BCUXX0a{)p(8H+S%L*< zKLf+mFZEwo*%(--F*7hZ@(S^?@p3W&jbKn^;$Yxn(_-j~XJp`5u=r$bNO7Fu&woN` za&sR{@6BYL^mIW?x|!4xp54rua~=h3;bd!wWGir(8M)*r`)!dk8)rF)nN0Zmym05D zRM*KxO5g4X2o;`UQ}}W0`oFh6iifPO{IG6*_%9{sI`@->^|N`!HaERvkTr`f{;!g{ z<^RHGOZ(28+auOv9ehQ$=4+q|+h#fE)}J%AY7Z(f^?3Fr z{cmuzSHZn!DjVM&nD{@rlg-sFY=w}W^ON}WE2d;__(`r``o?W`h>tTvTK# z!*l8B>DO&uY%ZEpu^@P6X`4u~g8l>XxNT+)Zyos6>x?!x20gs>dvewfVFl;J*T2uN zGtm5H8|S@VXt~J6Jxfii8^86XZdN+Ke^-6rTe-YB$w=Op;n?37!76!&FUR7QjXz~Ci zj95knEsOe%dnEmHoutjkb|Q|;_lnp2_`JJJMH>C@6Gh8Xf-;;x%c9S5B>_t zjMt7aGclAh=s;2)!@sP*z?27c1Oo#byFFVOD=_6TFmM2kZUWK@Jcb;aEDbjq7Tgml zN?O3l#AVHvxi UM25nGgAFb61`~Lh8WmZ10C`qAIsgCw literal 0 HcmV?d00001 diff --git a/src/unpack_utils.c b/src/unpack_utils.c index 7bb0bfa2..cd2f7551 100644 --- a/src/unpack_utils.c +++ b/src/unpack_utils.c @@ -129,11 +129,12 @@ uint32_t WavpackUnpackSamples (WavpackContext *wpc, int32_t *buffer, uint32_t sa WavpackStream *wps = wpc->streams [0]; int stream_index = 0; - // if the current block has no audio, or it's not the first block of a multichannel + // if we have no raw block buffered (e.g. a failed seek freed the streams), or the + // current block has no audio, or it's not the first block of a multichannel // sequence, or the sample we're on is past the last sample in this block...we need // to free up the streams and read the next block - if (!wps->wphdr.block_samples || !(wps->wphdr.flags & INITIAL_BLOCK) || + if (!wps->blockbuff || !wps->wphdr.block_samples || !(wps->wphdr.flags & INITIAL_BLOCK) || wps->sample_index >= GET_BLOCK_INDEX (wps->wphdr) + wps->wphdr.block_samples) { int64_t nexthdrpos; From 0dc7d15b22eaeb50c37c8fdb52c2d32fcaa3b874 Mon Sep 17 00:00:00 2001 From: Aizal Khan Date: Thu, 9 Jul 2026 13:12:09 +0530 Subject: [PATCH 2/2] drop the fuzzer decode-after-failed-seek pass and regression file The extra decode pass ran even when WavpackSeekSample64() returned FALSE, which is exactly the access the documentation forbids, so the harness itself violated the API contract. The regression file only meant anything with that pass in place. Keep just the blockbuff guard in WavpackUnpackSamples. --- fuzzing/fuzzer.cc | 13 ------------- .../seek-failure-decode-use-after-free.wv | Bin 886 -> 0 bytes 2 files changed, 13 deletions(-) delete mode 100644 fuzzing/regression/seek-failure-decode-use-after-free.wv diff --git a/fuzzing/fuzzer.cc b/fuzzing/fuzzer.cc index 991b625e..63aa1495 100644 --- a/fuzzing/fuzzer.cc +++ b/fuzzing/fuzzer.cc @@ -258,19 +258,6 @@ int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) ++tag_writes; } - // decode again from wherever the seek left us; this must stay safe even when the seek - // above failed (a failed seek must not leave the decoder pointing at freed block data) - - if (num_chans && num_chans <= 256) { - int32_t decoded_samples [BUF_SAMPLES * num_chans]; - int unpack_result; - - do { - unpack_result = WavpackUnpackSamples (wpc, decoded_samples, BUF_SAMPLES); - samples_decoded += unpack_result; - } while (unpack_result); - } - WavpackCloseFile (wpc); exit: diff --git a/fuzzing/regression/seek-failure-decode-use-after-free.wv b/fuzzing/regression/seek-failure-decode-use-after-free.wv deleted file mode 100644 index d9e0d4845794d132b3f059a197a6547376863dcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 886 zcmXRfE6C1cW?&FtVPJ4s!vF?585tN1C3lEypU(0>gQ+~Rj6qQ&$kWX&Vj~knxMP@W zR%u?g3`8kTGA*}6K>#Sr$iM_PR{*G&g+YKJC9xzC#Aad&5BCUXX0a{)p(8H+S%L*< zKLf+mFZEwo*%(--F*7hZ@(S^?@p3W&jbKn^;$Yxn(_-j~XJp`5u=r$bNO7Fu&woN` za&sR{@6BYL^mIW?x|!4xp54rua~=h3;bd!wWGir(8M)*r`)!dk8)rF)nN0Zmym05D zRM*KxO5g4X2o;`UQ}}W0`oFh6iifPO{IG6*_%9{sI`@->^|N`!HaERvkTr`f{;!g{ z<^RHGOZ(28+auOv9ehQ$=4+q|+h#fE)}J%AY7Z(f^?3Fr z{cmuzSHZn!DjVM&nD{@rlg-sFY=w}W^ON}WE2d;__(`r``o?W`h>tTvTK# z!*l8B>DO&uY%ZEpu^@P6X`4u~g8l>XxNT+)Zyos6>x?!x20gs>dvewfVFl;J*T2uN zGtm5H8|S@VXt~J6Jxfii8^86XZdN+Ke^-6rTe-YB$w=Op;n?37!76!&FUR7QjXz~Ci zj95knEsOe%dnEmHoutjkb|Q|;_lnp2_`JJJMH>C@6Gh8Xf-;;x%c9S5B>_t zjMt7aGclAh=s;2)!@sP*z?27c1Oo#byFFVOD=_6TFmM2kZUWK@Jcb;aEDbjq7Tgml zN?O3l#AVHvxi UM25nGgAFb61`~Lh8WmZ10C`qAIsgCw