airborne12 commented on code in PR #66877:
URL: https://github.com/apache/doris/pull/66877#discussion_r3848877905


##########
be/src/storage/index/snii/snii_index_reader.cpp:
##########
@@ -1100,11 +1126,35 @@ Status SniiIndexReader::_try_count_only_fastpath(
         RETURN_IF_ERROR(
                 ::doris::snii::query::count_only_term_df(*logical_reader, 
physical_term, &count));
     }
+    // df bounds the fabricated bitmap, so it has to be inside a document 
domain
+    // that is itself real. Two steps, because they fail differently.
+    const auto& stats = logical_reader->stats();
+    if (count > stats.doc_count || count > stats.indexed_doc_count) {
+        return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED, false>(
+                "SNII count fast path: term df {} exceeds document domain (doc 
count {}, "
+                "indexed doc count {})",
+                count, stats.doc_count, stats.indexed_doc_count);
+    }
+    // Both limits above are CRC-valid fields of the SAME image as df, so an 
image
+    // whose stats were inflated together with df clears them: on a real 10-row
+    // segment, df = doc_count = indexed_doc_count = 100 fabricates 100 ids, 
and
+    // SegmentIterator -- which seeds _row_bitmap with [0, num_rows) and 
intersects
+    // -- silently reports 10. The segment's own row count is the one bound the
+    // image cannot move. One-sided on purpose: an index covering FEWER rows 
than
+    // the segment still fabricates ids inside [0, num_rows), so only an 
oversized
+    // domain is corruption. This mirrors the equality SniiSegmentReader::
+    // load_inherited_index already demands of a rewrite.
+    if (stats.doc_count > _rows_of_segment) {
+        return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED, false>(
+                "SNII count fast path: index doc count {} exceeds the segment 
row count {}",
+                stats.doc_count, _rows_of_segment);
+    }
 
-    // Null handling. df is the exact match count REGARDLESS of nulls: the
-    // writer adds no tokens for a null doc (scalar add_nulls; a NULL array row
-    // is an empty range), so postings -- and therefore df -- never include
-    // null rows, exactly matching MATCH's "null never matches" semantics. The
+    // Null handling. df is the exact match count REGARDLESS of nulls: the 
writer

Review Comment:
   Two separate claims here; I ran both rather than reasoning about them. The 
scalar half is falsified by measurement, the frame-validation half is real but 
is not specific to this shortcut.
   
   **Scalar posting/null disjointness — measured, does not occur.** Your second 
example needs a CRC-valid scalar posting `{1,7}` overlapping a null frame 
`{1,3}`. I built exactly the adversarial input: a `Nullable(String)` column 
whose NULL row still carries a real nested value (`"alpha"`), i.e. what 
`default_implementation_for_nulls` leaves behind, and drove it through the 
production write path. Result:
   
   ```
   REAL SCALAR WRITER   posting(alpha) = []    null_bitmap = [0]
   ```
   
   No token is emitted for the null row, because both 
`ColumnWriter::append_nullable` overloads (`column_writer.cpp:386`, `:402`), 
`ScalarColumnWriter::append_nullable` (`:656`) and 
`IndexBuilder::_add_nullable` (`index_builder.cpp:1078`) split the batch into 
runs and route null runs to `add_nulls()`. Confirmed again at scale: 20000 rows 
with interleaved nulls, `posting & nulls` is empty for every term, in both 
`DictEntry` encodings.
   
   **The ARRAY counterpart, however, is real and reachable** — which is what 
this PR gates. Measured:
   
   ```
   array_concat(['alpha'], nullable_arr)   row 0: null_map=1  
nested=[alpha,zulu]
   REAL ARRAY WRITER                       posting(alpha) = [0]   null_bitmap = 
[0]
   ```
   
   So the shape exists, it comes from ordinary SQL 
(`PreparedFunctionImpl::default_implementation_for_nulls` keeps nested values 
on NULL rows), and the real writer does index the null row. The fast path now 
declines ARRAY columns on any segment with a null bitmap.
   
   **Frame vs Core consistency — you are right that the query path does not 
check, and it is worse than you describe.** The writer cannot emit an 
inconsistent frame (`stats.null_count`, the frame payload and `doc_count` all 
come from the same `null_docids_` vector), so the shape needs a tampered image. 
But the validation asymmetry is genuine: 
`LogicalIndexReader::read_null_docids()` checks doc count, cardinality and 
docid range (`logical_index_reader.cpp:619-635`) and its **only** caller is 
compaction (`snii_index_compaction.cpp:250`), while 
`SniiIndexReader::_read_null_bitmap()` checks nothing.
   
   The part worth flagging: that unvalidated read is not the fast path's. 
`read_null_bitmap()` and `query_with_null_bitmap()` — the source of the 
`mask_out_null` the row-accurate MATCH path applies — go through the same 
function. A corrupt frame therefore mis-masks the normal path too; the shortcut 
is not uniquely exposed. Lifting those three checks into `_read_null_bitmap()` 
is cheap (the frame is already fully decoded there) and would harden both 
paths, so I would rather do it as its own change than fold a normal-MATCH 
behaviour change into a reader-side bounds fix. Say the word and I will open it.
   
   The probes above are currently scratch. If they are wanted as regressions — 
the ARRAY one is the public-path case you asked for on the other thread — I can 
land them here; that costs a force-push and a fresh buildall on a PR that is 
currently green.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to