Medium watch CVSS 6.5 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in Video
DescriptionUninitialized resource in Video
ComponentVideo
Sub-componentVp8/mt
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker504633668
Fix commit8592391cdb3e (webm/libvpx) +105/-0
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-20 (per vendor advisory)
Fix landed2026-06-30 (public git)
Disclosed2026-08-25
Patch gap56 days — fix public in source before the advisory

Background

`MACROBLOCKD`
per-decode-context struct holding VP8 macroblock decoding state, including the corrupted flag that records whether a bool decoder overran its data.
`corrupted` flag
sticky boolean set when a bool (arithmetic) decoder reads past the end of its token partition; it gates error concealment and whether decoded output is trusted.
Worker `MACROBLOCKD` (`mb_row_di[i].mbd`)
a per-thread decode context in multithreaded VP8, allocated once and persisted for the decoder’s lifetime rather than reallocated per frame.
`setup_decoding_thread_data`
routine that seeds each worker’s mbd from the main-thread MACROBLOCKD (xd) at the start of every frame by copying a fixed list of fields.

Root Cause Analysis

The main thread resets xd->corrupted at the top of vp8_decode_frame on every frame, but the worker copies performed by setup_decoding_thread_data propagate only a fixed field list and omit corrupted. Because each worker MACROBLOCKD is allocated once and lives for the decoder’s lifetime, a corrupted bit set by a bool-decoder overrun on one frame is never cleared for subsequent frames.

As a result, a single malformed frame permanently poisons a worker’s corrupted state: every later frame decoded by that worker is treated as corrupt even when perfectly well-formed. When error concealment is enabled, this stale flag instead drives concealment logic on valid frames, causing the decoder to substitute stale or uninitialized worker state for legitimately decoded pixels.

The fix resets each worker’s mbd->corrupted at frame start — for keyframes, or for all frames when error concealment is enabled — mirroring the main-thread reset semantics.

Key insight
State that the main thread carefully re-initializes each frame was silently shared with persistent worker contexts through an incomplete field copy, so the invariant “corrupted reflects only the current frame” held on the main thread but was violated for every worker. The stickiness turned a transient per-frame condition into a durable, cross-frame corruption of validity tracking.

Attack Path

  1. Trip the flag Deliver a crafted VP8 frame whose token partition is short so a worker’s bool decoder overruns and sets that worker’s mbd->corrupted.
  2. Persist across frames Because setup_decoding_thread_data never resets it, the worker’s corrupted bit survives into subsequent, well-formed frames.
  3. Mishandle valid frames On later valid frames the stale flag either forces them to be reported corrupt or, with error concealment on, triggers concealment paths that read stale/uninitialized worker buffers.
  4. Disclose stale memory Concealed output built from uninitialized or previous-frame worker state is emitted as pixels, leaking residual heap/decoder memory into the rendered image.

Impact Assessment

An attacker-supplied VP8 stream can persistently corrupt a worker thread’s validity state, causing well-formed frames to be mishandled long after the malformed frame. With error concealment enabled this yields uninitialized-memory disclosure through decoded pixel output, potentially exposing stale heap contents. The condition is a correctness/information-disclosure bug in the multithreaded decode path rather than direct memory corruption, but it degrades output integrity and leaks resident memory.

Changed Functions

FunctionChangeNotes
for
vp8/decoder/threading.c
modified
if
vp8/decoder/threading.c
modified
TEST
test/decode_api_test.cc
modified
for
test/decode_api_test.cc
modified

Files Changed

  • vp8/decoder/threading.c
  • test/decode_api_test.cc

Audit Directions

  • Persistent-context field copies
    Audit every routine that seeds long-lived per-thread state from a per-frame master (setup_decoding_thread_data and peers) for fields the master resets each frame but the copy omits, especially sticky validity/error flags.
  • Reset-symmetry between main and worker paths
    Compare fields reset in vp8_decode_frame against those propagated to workers, flagging any state whose lifetime differs between the single-threaded and multithreaded contexts.
  • Concealment gated on stale flags
    Review error-concealment consumers of corrupted to confirm they cannot act on flags carried over from a prior frame, which would surface stale or uninitialized buffers as output.
From 8592391cdb3ef142c56d835788d71d6d4de36a63 Mon Sep 17 00:00:00 2001
From: Marco Paniconi <marpan@google.com>
Date: Tue, 30 Jun 2026 15:19:40 +0000
Subject: [PATCH] vp8/mt: reset worker mbd->corrupted at start of frame

Reset for keyframe, or all frames if error concealment
is enabled.

setup_decoding_thread_data() copies a fixed list of fields
from the main MACROBLOCKD into each worker's mb_row_di[i].mbd
at the start of every frame, but does not reset mbd->corrupted.
The worker MACROBLOCKDs are allocated once and persist for the
life of the decoder, whereas the main thread's xd->corrupted
is reset at the top of vp8_decode_frame().

If a worker's bool decoder runs past the end of its token
partition on one frame, all subsequent frames are reported as
corrupted even when well-formed.

Reset mbd->corrupted for every worker at the start of each
frame, mirroring the main-thread reset, but this should only
be done for keyframes or when error concealment is enabled.

Unittest added.

Bug: 504633668
Change-Id: I9120410035afedc77c4dbf868bcad5c534d98267
---
diff --git a/vp8/decoder/threading.c b/vp8/decoder/threading.c
index d16284d..d9182dd 100644
--- a/vp8/decoder/threading.c
+++ b/vp8/decoder/threading.c
@@ -47,6 +47,15 @@
 
   for (i = 0; i < count; ++i) {
     MACROBLOCKD *mbd = &mbrd[i].mbd;
+
+    // The worker MACROBLOCKDs persist across frames, so reset the per-frame
+    // error state in line with the main thread (see vp8_decode_frame()).
+    // This should only be done for keyframes or when error concealment
+    // is active.
+    if (pc->frame_type == KEY_FRAME || pbi->ec_active) {
+      mbd->corrupted = 0;
+    }
+
     mbd->subpixel_predict = xd->subpixel_predict;
     mbd->subpixel_predict8x4 = xd->subpixel_predict8x4;
     mbd->subpixel_predict8x8 = xd->subpixel_predict8x8;
diff --git a/test/decode_api_test.cc b/test/decode_api_test.cc
index 0f4639f..e33c463 100644
--- a/test/decode_api_test.cc
+++ b/test/decode_api_test.cc
@@ -9,13 +9,19 @@
  */
 
 #include <array>
+#include <vector>
 
 #include "gtest/gtest.h"
 
 #include "./vpx_config.h"
 #include "test/ivf_video_source.h"
+#include "test/video_source.h"
+#if CONFIG_VP8_ENCODER || CONFIG_VP9_ENCODER
+#include "vpx/vp8cx.h"
+#endif
 #include "vpx/vp8dx.h"
 #include "vpx/vpx_decoder.h"
+#include "vpx/vpx_encoder.h"
 
 namespace {
 
@@ -90,6 +96,96 @@
   EXPECT_EQ(VPX_CODEC_OK, vpx_codec_decode(&dec, nullptr, 0, nullptr, 0));
   EXPECT_EQ(VPX_CODEC_OK, vpx_codec_destroy(&dec));
 }
+
+#if CONFIG_VP8_ENCODER && CONFIG_MULTITHREAD
+// Encodes a sequence of key frames using two token partitions, truncates the
+// second token partition of one frame so that the worker thread's bool decoder
+// runs past the end of its partition, and then verifies that the
+// multi-threaded decoder still reports subsequent well-formed key frames as
+// not corrupted.
+TEST(DecodeAPI, Vp8MultiThreadedCorruptedStateResetAcrossFrames) {
+  constexpr int kWidth = 16;
+  constexpr int kHeight = 32;
+  constexpr int kFrames = 4;
+
+  std::vector<std::vector<uint8_t>> frames;
+  {
+    vpx_codec_ctx_t enc;
+    vpx_codec_enc_cfg_t cfg;
+    ASSERT_EQ(vpx_codec_enc_config_default(&vpx_codec_vp8_cx_algo, &cfg, 0),
+              VPX_CODEC_OK);
+    cfg.g_w = kWidth;
+    cfg.g_h = kHeight;
+    cfg.g_lag_in_frames = 0;
+    ASSERT_EQ(vpx_codec_enc_init(&enc, &vpx_codec_vp8_cx_algo, &cfg, 0),
+              VPX_CODEC_OK);
+    ASSERT_EQ(vpx_codec_control(&enc, VP8E_SET_TOKEN_PARTITIONS,
+                                VP8_TWO_TOKENPARTITION),
+              VPX_CODEC_OK);
+
+    libvpx_test::RandomVideoSource video;
+    video.SetSize(kWidth, kHeight);
+    video.set_limit(kFrames);
+    for (video.Begin(); video.img() != nullptr; video.Next()) {
+      ASSERT_EQ(
+          vpx_codec_encode(&enc, video.img(), video.pts(), video.duration(),
+                           VPX_EFLAG_FORCE_KF, VPX_DL_REALTIME),
+          VPX_CODEC_OK);
+      vpx_codec_iter_t iter = nullptr;
+      const vpx_codec_cx_pkt_t *pkt;
+      while ((pkt = vpx_codec_get_cx_data(&enc, &iter)) != nullptr) {
+        if (pkt->kind != VPX_CODEC_CX_FRAME_PKT) continue;
+        ASSERT_NE(pkt->data.frame.flags & VPX_FRAME_IS_KEY, 0u);
+        const uint8_t *buf = static_cast<const uint8_t *>(pkt->data.frame.buf);
+        frames.emplace_back(buf, buf + pkt->data.frame.sz);
+      }
+    }
+    ASSERT_EQ(vpx_codec_destroy(&enc), VPX_CODEC_OK);
+  }
+  ASSERT_EQ(frames.size(), static_cast<size_t>(kFrames));
+
+  // Truncate frame 1 so that only a single byte of the second token partition
+  // remains. The first token partition (used by the main thread) is left
+  // intact.
+  {
+    std::vector<uint8_t> &f = frames[1];
+    ASSERT_GT(f.size(), 10u);
+    const size_t first_part_sz =
+        (static_cast<size_t>(f[0]) | (static_cast<size_t>(f[1]) << 8) |
+         (static_cast<size_t>(f[2]) << 16)) >>
+        5;
+    const size_t part_sizes = 10 + first_part_sz;
+    ASSERT_LT(part_sizes + 3, f.size());
+    const size_t part0_sz = static_cast<size_t>(f[part_sizes]) |
+                            (static_cast<size_t>(f[part_sizes + 1]) << 8) |
+                            (static_cast<size_t>(f[part_sizes + 2]) << 16);
+    const size_t part1_start = part_sizes + 3 + part0_sz;
+    ASSERT_LT(part1_start, f.size());
+    f.resize(part1_start + 1);
+  }
+
+  vpx_codec_ctx_t dec;
+  vpx_codec_dec_cfg_t dec_cfg = { /*threads=*/4, /*w=*/0, /*h=*/0 };
+  ASSERT_EQ(vpx_codec_dec_init(&dec, &vpx_codec_vp8_dx_algo, &dec_cfg, 0),
+            VPX_CODEC_OK);
+  for (int i = 0; i < kFrames; ++i) {
+    const vpx_codec_err_t res = vpx_codec_decode(
+        &dec, frames[i].data(), static_cast<unsigned int>(frames[i].size()),
+        /*user_priv=*/nullptr, /*deadline=*/0);
+    vpx_codec_iter_t iter = nullptr;
+    while (vpx_codec_get_frame(&dec, &iter)) {
+    }
+    if (i == 1) continue;
+    EXPECT_EQ(res, VPX_CODEC_OK)
+        << "frame " << i << ": " << vpx_codec_error_detail(&dec);
+    int corrupted = -1;
+    EXPECT_EQ(vpx_codec_control(&dec, VP8D_GET_FRAME_CORRUPTED, &corrupted),
+              VPX_CODEC_OK);
+    EXPECT_EQ(corrupted, 0) << "frame " << i;
+  }
+  EXPECT_EQ(vpx_codec_destroy(&dec), VPX_CODEC_OK);
+}
+#endif  // CONFIG_VP8_ENCODER && CONFIG_MULTITHREAD
 #endif  // CONFIG_VP8_DECODER
 
 #if CONFIG_VP9_DECODER
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/decode_api_test.cc b/test/decode_api_test.cc
index 0f4639f..e33c463 100644
--- a/test/decode_api_test.cc
+++ b/test/decode_api_test.cc
@@ -9,13 +9,19 @@
  */
 
 #include <array>
+#include <vector>
 
 #include "gtest/gtest.h"
 
 #include "./vpx_config.h"
 #include "test/ivf_video_source.h"
+#include "test/video_source.h"
+#if CONFIG_VP8_ENCODER || CONFIG_VP9_ENCODER
+#include "vpx/vp8cx.h"
+#endif
 #include "vpx/vp8dx.h"
 #include "vpx/vpx_decoder.h"
+#include "vpx/vpx_encoder.h"
 
 namespace {
 
@@ -90,6 +96,96 @@
   EXPECT_EQ(VPX_CODEC_OK, vpx_codec_decode(&dec, nullptr, 0, nullptr, 0));
   EXPECT_EQ(VPX_CODEC_OK, vpx_codec_destroy(&dec));
 }
+
+#if CONFIG_VP8_ENCODER && CONFIG_MULTITHREAD
+// Encodes a sequence of key frames using two token partitions, truncates the
+// second token partition of one frame so that the worker thread's bool decoder
+// runs past the end of its partition, and then verifies that the
+// multi-threaded decoder still reports subsequent well-formed key frames as
+// not corrupted.
+TEST(DecodeAPI, Vp8MultiThreadedCorruptedStateResetAcrossFrames) {
+  constexpr int kWidth = 16;
+  constexpr int kHeight = 32;
+  constexpr int kFrames = 4;
+
+  std::vector<std::vector<uint8_t>> frames;
+  {
+    vpx_codec_ctx_t enc;
+    vpx_codec_enc_cfg_t cfg;
+    ASSERT_EQ(vpx_codec_enc_config_default(&vpx_codec_vp8_cx_algo, &cfg, 0),
+              VPX_CODEC_OK);
+    cfg.g_w = kWidth;
+    cfg.g_h = kHeight;
+    cfg.g_lag_in_frames = 0;
+    ASSERT_EQ(vpx_codec_enc_init(&enc, &vpx_codec_vp8_cx_algo, &cfg, 0),
+              VPX_CODEC_OK);
+    ASSERT_EQ(vpx_codec_control(&enc, VP8E_SET_TOKEN_PARTITIONS,
+                                VP8_TWO_TOKENPARTITION),
+              VPX_CODEC_OK);
+
+    libvpx_test::RandomVideoSource video;
+    video.SetSize(kWidth, kHeight);
+    video.set_limit(kFrames);
+    for (video.Begin(); video.img() != nullptr; video.Next()) {
+      ASSERT_EQ(
+          vpx_codec_encode(&enc, video.img(), video.pts(), video.duration(),
+                           VPX_EFLAG_FORCE_KF, VPX_DL_REALTIME),
+          VPX_CODEC_OK);
+      vpx_codec_iter_t iter = nullptr;
+      const vpx_codec_cx_pkt_t *pkt;
+      while ((pkt = vpx_codec_get_cx_data(&enc, &iter)) != nullptr) {
+        if (pkt->kind != VPX_CODEC_CX_FRAME_PKT) continue;
+        ASSERT_NE(pkt->data.frame.flags & VPX_FRAME_IS_KEY, 0u);
+        const uint8_t *buf = static_cast<const uint8_t *>(pkt->data.frame.buf);
+        frames.emplace_back(buf, buf + pkt->data.frame.sz);
+      }
+    }
+    ASSERT_EQ(vpx_codec_destroy(&enc), VPX_CODEC_OK);
+  }
+  ASSERT_EQ(frames.size(), static_cast<size_t>(kFrames));
+
+  // Truncate frame 1 so that only a single byte of the second token partition
+  // remains. The first token partition (used by the main thread) is left
+  // intact.
+  {
+    std::vector<uint8_t> &f = frames[1];
+    ASSERT_GT(f.size(), 10u);
+    const size_t first_part_sz =
+        (static_cast<size_t>(f[0]) | (static_cast<size_t>(f[1]) << 8) |
+         (static_cast<size_t>(f[2]) << 16)) >>
+        5;
+    const size_t part_sizes = 10 + first_part_sz;
+    ASSERT_LT(part_sizes + 3, f.size());
+    const size_t part0_sz = static_cast<size_t>(f[part_sizes]) |
+                            (static_cast<size_t>(f[part_sizes + 1]) << 8) |
+                            (static_cast<size_t>(f[part_sizes + 2]) << 16);
+    const size_t part1_start = part_sizes + 3 + part0_sz;
+    ASSERT_LT(part1_start, f.size());
+    f.resize(part1_start + 1);
+  }
+
+  vpx_codec_ctx_t dec;
+  vpx_codec_dec_cfg_t dec_cfg = { /*threads=*/4, /*w=*/0, /*h=*/0 };
+  ASSERT_EQ(vpx_codec_dec_init(&dec, &vpx_codec_vp8_dx_algo, &dec_cfg, 0),
+            VPX_CODEC_OK);
+  for (int i = 0; i < kFrames; ++i) {
+    const vpx_codec_err_t res = vpx_codec_decode(
+        &dec, frames[i].data(), static_cast<unsigned int>(frames[i].size()),
+        /*user_priv=*/nullptr, /*deadline=*/0);
+    vpx_codec_iter_t iter = nullptr;
+    while (vpx_codec_get_frame(&dec, &iter)) {
+    }
+    if (i == 1) continue;
+    EXPECT_EQ(res, VPX_CODEC_OK)
+        << "frame " << i << ": " << vpx_codec_error_detail(&dec);
+    int corrupted = -1;
+    EXPECT_EQ(vpx_codec_control(&dec, VP8D_GET_FRAME_CORRUPTED, &corrupted),
+              VPX_CODEC_OK);
+    EXPECT_EQ(corrupted, 0) << "frame " << i;
+  }
+  EXPECT_EQ(vpx_codec_destroy(&dec), VPX_CODEC_OK);
+}
+#endif  // CONFIG_VP8_ENCODER && CONFIG_MULTITHREAD
 #endif  // CONFIG_VP8_DECODER
 
 #if CONFIG_VP9_DECODER
Loading diff…

Original Bug Report

reported by vm...@google.com

Libvpx VP8 MT decoder uninitialized heap memory leak

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: In the multithreaded VP8 decoder, worker threads do not reset their internal corrupted flag between frames. If a worker encounters an error, it skips decoding its assigned regions for all subsequent frames. When the video resolution changes, new frame buffers are allocated without zero-initialization, leading to a leak of uninitialized PartitionAlloc memory to JavaScript.

Affected files:

  • third_party/libvpx/source/libvpx/vp8/decoder/threading.c
  • third_party/libvpx/source/libvpx/vp8/decoder/decodeframe.c
  • third_party/libvpx/source/libvpx/vpx_scale/generic/yv12config.c
  • third_party/libvpx/source/libvpx/vp8/vp8_dx_iface.c
  • media/filters/vpx_video_decoder.cc

Estimated timestamp from git blame: 2023-06-07

Summary

A vulnerability in the multithreaded VP8 decoder in libvpx causes a corruption flag in worker threads to persist across frames. When a worker thread encounters a malformed bitstream, its internal corrupted flag is set. Due to a missing reset in the per-frame setup routine, this flag remains set for all subsequent frames. This causes the worker thread to skip decoding its assigned macroblock rows in every future frame. Since VP8 frame buffers are allocated via vpx_memalign (using PartitionAlloc in Chrome) without zero-initialization, the skipped regions contain raw heap memory. Chrome’s VpxVideoDecoder surfaces these uninitialized regions to JavaScript, enabling an information leak of renderer-process heap memory.

Root Cause Analysis

In third_party/libvpx/source/libvpx/vp8/decoder/threading.c, the setup_decoding_thread_data function prepares worker threads for each frame by copying fields from the main thread’s MACROBLOCKD structure. However, it fails to reset or copy the mbd->corrupted flag:

static void setup_decoding_thread_data(VP8D_COMP *pbi, MACROBLOCKD *xd,
                                       MB_ROW_DEC *mbrd, int count) {
  for (i = 0; i < count; ++i) {
    MACROBLOCKD *mbd = &mbrd[i].mbd;
    mbd->subpixel_predict      = xd->subpixel_predict;
    // ... (other fields initialized, but mbd->corrupted is omitted)
  }
}

In contrast, the main thread’s xd->corrupted is explicitly reset at the start of every frame in third_party/libvpx/source/libvpx/vp8/decoder/decodeframe.c (line 896).

During decoding, if a worker thread sets xd->corrupted to 1 (e.g., due to a bitstream error in its assigned partition), it enters an error path (threading.c:409) that calls vpx_internal_error(). This performs a longjmp back to the thread’s setjmp point. Because the flag is never reset in setup_decoding_thread_data, once a worker’s corrupted bit is set, it becomes “sticky.” For all subsequent frames, the worker immediately sees xd->corrupted == 1, calls vpx_internal_error(), and aborts decoding its assigned interleaved rows before any pixel data is written.

Heap Memory Leak Mechanism

  1. Uninitialized Allocation: When a video resolution change is triggered by a new keyframe, libvpx re-allocates its frame buffers via vp8_yv12_alloc_frame_buffer. This relies on vpx_memalign, which uses malloc (mapped to PartitionAlloc). In production Chrome builds, these allocations are not zero-initialized (yv12config.c:72-79).
  2. Skipped Rows: The worker with the sticky corrupted state immediately bails on the new frame, leaving its portion of the newly allocated buffer as untouched, raw PartitionAlloc memory.
  3. Exposure to JavaScript: vp8_decode_frame successfully parses the keyframe header and returns 0 (success), which translates to a VPX_CODEC_OK return from vpx_codec_decode. Chrome’s VpxVideoDecoder (media/filters/vpx_video_decoder.cc) assumes success and does not query the VP8D_GET_FRAME_CORRUPTED control. It wraps or copies the vpx_image_t into a media::VideoFrame and surfaces it to the media pipeline.
  4. Exfiltration: A web page can read these uninitialized bytes using WebCodecs (VideoFrame.copyTo()) or by drawing the video to a <canvas> and calling getImageData(). By manipulating the video resolution, an attacker can target specific PartitionAlloc bucket sizes to leak pointers or sensitive data from recently freed objects.

Suggested Reproduction Steps

(Note: These are potential steps, as our tooling cannot currently execute a PoC)

  1. Host a WebM file with a VP8 track configured for multithreaded decoding (e.g., multiple token partitions).
  2. Provide an initial valid keyframe to establish the decoder state and worker threads.
  3. Provide a malformed frame with a bitstream error in one of the partitions. This causes the assigned worker thread to set its corrupted flag.
  4. Provide a valid keyframe at a new resolution. This forces a buffer reallocation.
  5. The macroblock rows assigned to the corrupted worker thread will contain uninitialized heap data from the freshly allocated buffer.
  6. In JavaScript, use a <video> element or WebCodecs to decode the stream and exfiltrate the uninitialized data from the affected rows using a <canvas> or the copyTo() method.

Suggested Fix

Explicitly initialize the corrupted flag in the worker threads’ setup routine.

In third_party/libvpx/source/libvpx/vp8/decoder/threading.c:

static void setup_decoding_thread_data(VP8D_COMP *pbi, MACROBLOCKD *xd,
                                       MB_ROW_DEC *mbrd, int count) {
  for (i = 0; i < count; ++i) {
    MACROBLOCKD *mbd = &mbrd[i].mbd;
    mbd->subpixel_predict      = xd->subpixel_predict;
    // ... existing initializations ...
    mbd->corrupted = 0; // Explicitly reset the corrupted flag
  }
}

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker