Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions sherpa-onnx/c-api/c-api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,24 @@ const SherpaOnnxOfflineRecognizerResult *SherpaOnnxGetOfflineStreamResult(
r->ys_log_probs = nullptr;
}

// Copy vocab_log_probs (flattened row-major: count * vocab_size)
if (!result.vocab_log_probs.empty() &&
static_cast<int32_t>(result.vocab_log_probs.size()) == r->count &&
!result.vocab_log_probs[0].empty()) {
int32_t vocab_size =
static_cast<int32_t>(result.vocab_log_probs[0].size());
r->vocab_size = vocab_size;
float *flat = new float[r->count * vocab_size];
for (int32_t i = 0; i < r->count; ++i) {
std::copy(result.vocab_log_probs[i].begin(),
result.vocab_log_probs[i].end(), flat + i * vocab_size);
}
Comment on lines +868 to +879

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Validate every row width before flattening to avoid buffer corruption.

At Line 877, the copy assumes every result.vocab_log_probs[i] has vocab_size elements (from Line 873), but only row 0 is validated. A wider row can overrun flat; a shorter row yields partial/uninitialized output. Also, Line 875 multiplies int32_t dimensions without overflow guarding.

Suggested fix
-  if (!result.vocab_log_probs.empty() &&
-      static_cast<int32_t>(result.vocab_log_probs.size()) == r->count &&
-      !result.vocab_log_probs[0].empty()) {
-    int32_t vocab_size =
-        static_cast<int32_t>(result.vocab_log_probs[0].size());
-    r->vocab_size = vocab_size;
-    float *flat = new float[r->count * vocab_size];
-    for (int32_t i = 0; i < r->count; ++i) {
-      std::copy(result.vocab_log_probs[i].begin(),
-                result.vocab_log_probs[i].end(), flat + i * vocab_size);
-    }
-    r->vocab_log_probs = flat;
+  if (!result.vocab_log_probs.empty() &&
+      static_cast<int32_t>(result.vocab_log_probs.size()) == r->count &&
+      !result.vocab_log_probs[0].empty()) {
+    const size_t rows = result.vocab_log_probs.size();
+    const size_t vocab_size = result.vocab_log_probs[0].size();
+    const bool uniform = std::all_of(
+        result.vocab_log_probs.begin(), result.vocab_log_probs.end(),
+        [vocab_size](const std::vector<float> &row) {
+          return row.size() == vocab_size;
+        });
+
+    if (uniform && rows > 0 &&
+        vocab_size <= std::numeric_limits<size_t>::max() / rows) {
+      r->vocab_size = static_cast<int32_t>(vocab_size);
+      float *flat = new float[rows * vocab_size];
+      for (size_t i = 0; i < rows; ++i) {
+        std::copy(result.vocab_log_probs[i].begin(),
+                  result.vocab_log_probs[i].end(), flat + i * vocab_size);
+      }
+      r->vocab_log_probs = flat;
+    } else {
+      r->vocab_log_probs = nullptr;
+      r->vocab_size = 0;
+    }
   } else {
     r->vocab_log_probs = nullptr;
     r->vocab_size = 0;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/c-api/c-api.cc` around lines 868 - 879, The code flattens
result.vocab_log_probs into flat assuming every row matches vocab_size and
multiplies r->count*vocab_size without overflow checks; to fix, before
allocating and copying, validate that result.vocab_log_probs.size() ==
static_cast<size_t>(r->count), compute vocab_size from row 0, then loop over i
and verify result.vocab_log_probs[i].size() == static_cast<size_t>(vocab_size)
(handle mismatch by returning an error/cleanup), and check/mask the
multiplication r->count * vocab_size for overflow (or use size_t and
bounds-check against numeric_limits<size_t>::max()) before new float[...] to
avoid buffer overruns when filling flat and ensure safe memory allocation for
flat and correct assignment to r->vocab_size.

r->vocab_log_probs = flat;
Comment on lines +875 to +880

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Potential integer overflow in the calculation of the buffer size and pointer arithmetic. r->count and vocab_size are both int32_t, so their product is calculated as a 32-bit signed integer. If the product exceeds INT32_MAX (approx. 2.1 billion), it will overflow, leading to an incorrect allocation size and undefined behavior during indexing in the subsequent loop. This is a realistic scenario for long audio files or large vocabularies.

    float *flat = new float[static_cast<size_t>(r->count) * vocab_size];
    for (int32_t i = 0; i < r->count; ++i) {
      std::copy(result.vocab_log_probs[i].begin(),
                result.vocab_log_probs[i].end(),
                flat + static_cast<size_t>(i) * vocab_size);
    }
    r->vocab_log_probs = flat;

} else {
r->vocab_log_probs = nullptr;
r->vocab_size = 0;
}

// Copy segment-level timestamps (from Whisper with segment timestamps)
auto segment_count = result.segment_texts.size();
if (segment_count > 0 && result.segment_timestamps.size() == segment_count &&
Expand Down Expand Up @@ -908,6 +926,8 @@ const SherpaOnnxOfflineRecognizerResult *SherpaOnnxGetOfflineStreamResult(
r->segment_texts_arr = nullptr;
}

// NB: vocab_log_probs / vocab_size are set above (before the segment block).

return r;
}

Expand All @@ -928,6 +948,7 @@ void SherpaOnnxDestroyOfflineRecognizerResult(
delete[] r->segment_durations;
delete[] r->segment_texts;
delete[] r->segment_texts_arr;
delete[] r->vocab_log_probs;
delete r;
}
}
Expand Down
15 changes: 15 additions & 0 deletions sherpa-onnx/c-api/c-api.h
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,21 @@ typedef struct SherpaOnnxOfflineRecognizerResult {

/** Number of segment entries in the segment-level arrays. */
int32_t segment_count;

/**
* Optional flattened vocabulary log-probability matrix.
*
* When non-NULL, this is a contiguous row-major array of
* @c count * @c vocab_size floats. Row @c i (starting at offset
* <code>i * vocab_size</code>) holds the full log-probability distribution
* over the vocabulary for the @c i-th emitted token.
*
* Used for entropy-based confidence estimation.
*/
const float *vocab_log_probs;

/** Vocabulary size (number of columns in @c vocab_log_probs). */
int32_t vocab_size;
} SherpaOnnxOfflineRecognizerResult;

/**
Expand Down
11 changes: 11 additions & 0 deletions sherpa-onnx/csrc/offline-ctc-decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ struct OfflineCtcDecoderResult {
///
/// tokens.size() == timestamps.size()
std::vector<int32_t> timestamps;

/// Greedy log-probability of tokens[i] (i.e. log p(tokens[i] | frame)).
/// Empty when the decoder did not record it.
/// If populated, token_log_probs.size() == tokens.size().
std::vector<float> token_log_probs;

/// Full vocabulary log-probability distribution at the emission frame of
/// tokens[i]. vocab_log_probs[i].size() == vocab_size.
/// Empty when the decoder did not record it.
/// If populated, vocab_log_probs.size() == tokens.size().
std::vector<std::vector<float>> vocab_log_probs;
};

class OfflineCtcDecoder {
Expand Down
5 changes: 4 additions & 1 deletion sherpa-onnx/csrc/offline-ctc-greedy-search-decoder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ std::vector<OfflineCtcDecoderResult> OfflineCtcGreedySearchDecoder::Decode(
std::max_element(
static_cast<const float *>(p_log_probs),
static_cast<const float *>(p_log_probs) + vocab_size)));
p_log_probs += vocab_size;
float log_prob = p_log_probs[y];

if (y != blank_id_ && y != prev_id) {
r.tokens.push_back(y);
r.timestamps.push_back(t);
r.token_log_probs.push_back(log_prob);
r.vocab_log_probs.emplace_back(p_log_probs, p_log_probs + vocab_size);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Unconditionally populating vocab_log_probs for every emitted token can lead to excessive memory consumption, especially for long audio inputs and models with large vocabularies. For instance, a 1-hour recording with 10,000 tokens and a 50,000-word vocabulary would require approximately 2GB of additional memory. Since this data is only needed for specific use cases like entropy-based confidence estimation, it should be optional. Additionally, using std::vector<std::vector> results in many small allocations; a single flat vector would be more efficient.

}
p_log_probs += vocab_size;
prev_id = y;
} // for (int32_t t = 0; ...)

Expand Down
10 changes: 10 additions & 0 deletions sherpa-onnx/csrc/offline-recognizer-ctc-impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ OfflineRecognitionResult Convert(const OfflineCtcDecoderResult &src,

r.words = std::move(src.words);

if (!src.token_log_probs.empty() &&
src.token_log_probs.size() == src.tokens.size()) {
r.ys_log_probs = src.token_log_probs;
}

if (!src.vocab_log_probs.empty() &&
src.vocab_log_probs.size() == src.tokens.size()) {
r.vocab_log_probs = src.vocab_log_probs;
}
Comment on lines +80 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep log-prob arrays aligned with filtered output tokens

Convert() filters tokens before writing r.tokens, but these assignments copy arrays indexed by src.tokens. That can break the per-token alignment contract for r.ys_log_probs/r.vocab_log_probs when tokens are skipped.

💡 Suggested fix
@@
   OfflineRecognitionResult r;
   r.tokens.reserve(src.tokens.size());
   r.timestamps.reserve(src.timestamps.size());
+  std::vector<int32_t> kept_indices;
+  kept_indices.reserve(src.tokens.size());
@@
     auto sym = sym_table[src.tokens[i]];
@@
     r.tokens.push_back(std::move(sym));
+    kept_indices.push_back(i);
   }
@@
-  if (!src.token_log_probs.empty() &&
-      src.token_log_probs.size() == src.tokens.size()) {
-    r.ys_log_probs = src.token_log_probs;
+  if (!src.token_log_probs.empty() &&
+      src.token_log_probs.size() == src.tokens.size()) {
+    r.ys_log_probs.reserve(kept_indices.size());
+    for (auto idx : kept_indices) {
+      r.ys_log_probs.push_back(src.token_log_probs[idx]);
+    }
   }
 
   if (!src.vocab_log_probs.empty() &&
       src.vocab_log_probs.size() == src.tokens.size()) {
-    r.vocab_log_probs = src.vocab_log_probs;
+    r.vocab_log_probs.reserve(kept_indices.size());
+    for (auto idx : kept_indices) {
+      r.vocab_log_probs.push_back(src.vocab_log_probs[idx]);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/csrc/offline-recognizer-ctc-impl.h` around lines 80 - 88,
Convert() is copying src.token_log_probs and src.vocab_log_probs directly into
r.ys_log_probs and r.vocab_log_probs using src.tokens indices, which breaks
alignment when Convert() filters tokens before assigning r.tokens; update the
logic in Convert() to build r.ys_log_probs and r.vocab_log_probs by iterating
the same filtered token indices used to produce r.tokens and push the
corresponding entries from src.token_log_probs and src.vocab_log_probs (only
when those source arrays are non-empty and have sufficient size), so the
resulting r.ys_log_probs and r.vocab_log_probs match r.tokens in length and
order.


return r;
}

Expand Down
7 changes: 7 additions & 0 deletions sherpa-onnx/csrc/offline-stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ struct OfflineRecognitionResult {
/// ys_log_probs[i] contains the log probability (confidence) for tokens[i].
std::vector<float> ys_log_probs;

/// vocab_log_probs[i] contains the full log-probability distribution over
/// the vocabulary at the emission frame of tokens[i]. Used for entropy-based
/// confidence estimation. Empty when not provided by the decoder.
/// If populated, vocab_log_probs.size() == tokens.size() and every inner
/// vector has the same length (the model vocab size).
std::vector<std::vector<float>> vocab_log_probs;

// Word IDs from FST decoding (CTC models with FST decoder only).
std::vector<int32_t> words;

Expand Down