Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ number says nothing about any other port. See the Releases section of

## [Unreleased]

### Fixed

- **`DatasetReader` reads a row under the caller's `max_bytes_literal_length`** (#160), as protowire-go does. A row's cells were parsed under the default `kMaxBytesLiteralLength` whatever the options said. A caller who lowered the limit still got a row whose `b"…"` cell was past it, and a caller who raised it had that cell refused. `0` still means the default. Only the nesting depth used to reach the row parser. The header (a `@header` block and the `@dataset` line) was parsed under the defaults for both limits. It is now parsed under the caller's `max_nesting_depth` and `max_bytes_literal_length`, so `Create` refuses such a header as protowire-go's `NewDatasetReader` does, with the same text and position.

## [2.0.0] — 2026-11-05

A major release, because `pb::Marshal` now returns `StatusOr<std::vector<uint8_t>>` and every caller must change (#93). `pb::Marshal` and `pxf::Marshal` refuse what a decoder at the default limits refuses, instead of writing it. A PXF `BigFloat` literal is refused in protowire-go's words and order of checks, and a big-number message no longer counts as a nesting level. Several changes narrow what decodes, encodes or binds; each says so. This release also carries the change prepared as 1.2.1, which was never tagged.
Expand Down
11 changes: 9 additions & 2 deletions src/pxf/dataset_reader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,9 @@ Status DatasetReader::Next(DatasetRow* out) {
auto parsed =
ParseDatasetRow(row_bytes,
static_cast<int>(columns_.size()),
opts_.max_nesting_depth > 0 ? opts_.max_nesting_depth : kMaxNestingDepth);
opts_.max_nesting_depth > 0 ? opts_.max_nesting_depth : kMaxNestingDepth,
opts_.max_bytes_literal_length > 0 ? opts_.max_bytes_literal_length
: kMaxBytesLiteralLength);
// Advance past the consumed bytes whether parse succeeded or not
// — on failure we don't want to retry the same bad row forever.
pending_.erase(0, static_cast<size_t>(end) + 1);
Expand Down Expand Up @@ -560,7 +562,12 @@ Status DatasetReader::ReadHeader() {
if (!scan_err.ok()) return scan_err;
if (found) {
std::string_view header(pending_.data(), static_cast<size_t>(header_end) + 1);
auto doc = Parse(header);
// The header is parsed under the caller's limits, as the rows are and
// as protowire-go parses both (#160).
ParseOptions popts;
popts.max_nesting_depth = opts_.max_nesting_depth;
popts.max_bytes_literal_length = opts_.max_bytes_literal_length;
auto doc = Parse(header, popts);
if (!doc.ok()) return doc.status();
if (doc->datasets.empty()) {
return Status::Error("pxf: no @dataset directive in stream");
Expand Down
7 changes: 5 additions & 2 deletions src/pxf/parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1153,8 +1153,11 @@ StatusOr<DatasetDirective> ParseDatasetAt(Lexer* lex, Token* current, int max_de
return tbl;
}

StatusOr<DatasetRow> ParseDatasetRow(std::string_view row, int expected, int max_depth) {
Parser p(row, max_depth, kMaxBytesLiteralLength);
StatusOr<DatasetRow> ParseDatasetRow(std::string_view row,
int expected,
int max_depth,
int max_bytes_literal) {
Parser p(row, max_depth, max_bytes_literal);
int end = 0;
return p.ParseTableRow(expected, &end);
}
Expand Down
8 changes: 6 additions & 2 deletions src/pxf/parser_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ namespace protowire::pxf {
// token *lex produces, and leaves both on the token after it.
StatusOr<DatasetDirective> ParseDatasetAt(Lexer* lex, Token* current, int max_depth);

// ParseDatasetRow parses one `( cell, ... )` of expected cells.
StatusOr<DatasetRow> ParseDatasetRow(std::string_view row, int expected, int max_depth);
// ParseDatasetRow parses one `( cell, ... )` of expected cells, under the
// caller's nesting and bytes-literal limits.
StatusOr<DatasetRow> ParseDatasetRow(std::string_view row,
int expected,
int max_depth,
int max_bytes_literal);

// CheckDescriptorSchema runs ValidateDescriptor and joins any violations
// into one error, the bind-time check Unmarshal and DatasetReader share.
Expand Down
53 changes: 53 additions & 0 deletions test/pxf_hardening_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "protowire/limits.h"
#include "protowire/pxf.h"
#include "protowire/pxf/dataset_reader.h"
#include "protowire/pxf/lexer.h"
#include "protowire/pxf/parser.h"

Expand All @@ -17,6 +18,7 @@
#include "protoc_compat.h"

#include <memory>
#include <sstream>
#include <string>

#include <google/protobuf/compiler/importer.h>
Expand Down Expand Up @@ -192,6 +194,57 @@ TEST_F(Hardening, BytesLiteralLengthBound) {
EXPECT_TRUE(protowire::pxf::Parse(doc).ok());
}

// A dataset row is read under the caller's limit too, as protowire-go reads
// it (#160). It used to be read under the default whatever the options said.
TEST(HardeningDataset, RowBytesLiteralUnderTheCallersLimit) {
// 256 characters of base64 decode to 192 bytes.
const std::string doc = "@dataset x.Row ( raw )\n( b\"" + std::string(256, 'A') + "\" )\n";
auto first_row = [&](int limit) {
std::istringstream in(doc);
UnmarshalOptions opts;
opts.max_bytes_literal_length = limit;
auto tr = protowire::pxf::DatasetReader::Create(&in, opts);
EXPECT_TRUE(tr.ok()) << tr.status().ToString();
protowire::pxf::DatasetRow row;
return (*tr)->Next(&row);
};
protowire::Status st = first_row(191);
ASSERT_FALSE(st.ok());
EXPECT_NE(st.message().find("bytes literal decodes to more than MaxBytesLiteralLength=191 bytes"),
std::string::npos)
<< st.ToString();
EXPECT_TRUE(first_row(192).ok()) << first_row(192).ToString();
EXPECT_TRUE(first_row(0).ok()) << first_row(0).ToString(); // 0 is the default
}

// The header too: a @header block before @dataset is parsed under the
// caller's limits, so Create refuses it as protowire-go's NewDatasetReader does.
TEST(HardeningDataset, HeaderUnderTheCallersLimits) {
const std::string bytes_doc =
"@header x.H { raw = b\"" + std::string(256, 'A') + "\" }\n@dataset x.Row ( raw )\n( 1 )\n";
const std::string deep_doc =
"@header x.H { a { b { c { d = 1 } } } }\n@dataset x.Row ( d )\n( 1 )\n";
auto create = [](const std::string& doc, UnmarshalOptions opts) {
std::istringstream in(doc);
return protowire::pxf::DatasetReader::Create(&in, opts).status();
};
UnmarshalOptions opts;
opts.max_bytes_literal_length = 191;
protowire::Status st = create(bytes_doc, opts);
ASSERT_FALSE(st.ok());
EXPECT_NE(st.message().find("bytes literal decodes to more than MaxBytesLiteralLength=191 bytes"),
std::string::npos)
<< st.ToString();
EXPECT_TRUE(create(bytes_doc, {}).ok());
opts = {};
opts.max_nesting_depth = 3;
st = create(deep_doc, opts);
ASSERT_FALSE(st.ok());
EXPECT_NE(st.message().find("MaxNestingDepth=3"), std::string::npos) << st.ToString();
opts.max_nesting_depth = 4;
EXPECT_TRUE(create(deep_doc, opts).ok()) << create(deep_doc, opts).ToString();
}

TEST_F(Hardening, RepeatedCountBound) {
std::string list = "values = [";
for (int i = 0; i < 16; ++i) list += (i ? ", " : "") + std::to_string(i);
Expand Down
Loading