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
8 changes: 5 additions & 3 deletions src/core/gzip/deflate.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <algorithm> // std::min
#include <array> // std::array
#include <cassert> // assert
#include <cstddef> // std::size_t
#include <cstdint> // std::uint8_t, std::uint16_t
#include <cstring> // std::memcpy
Expand Down Expand Up @@ -213,16 +214,17 @@ class DeflateDecoder {
}
all_lengths[index++] = 0;
}
} else if (symbol == 18) {
} else {
// The code length tree is built over a 19 symbol alphabet, so its
// decoder can never hand back anything past symbol 18
assert(symbol == 18);
const auto repeats{this->reader_->read_bits(7) + 11};
for (std::size_t step = 0; step < repeats; ++step) {
if (index >= all_lengths.size()) {
throw GZIPError{"Code length count overflow"};
}
all_lengths[index++] = 0;
}
} else {
throw GZIPError{"Invalid code length symbol"};
}
}

Expand Down
8 changes: 5 additions & 3 deletions src/core/gzip/huffman.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <algorithm> // std::ranges::fill
#include <array> // std::array
#include <cassert> // assert
#include <cstddef> // std::size_t
#include <cstdint> // std::uint8_t, std::uint16_t

Expand Down Expand Up @@ -37,9 +38,10 @@ class HuffmanDecoder {
std::ranges::fill(this->lut_, std::uint16_t{0});

for (std::size_t symbol = 0; symbol < length_count; ++symbol) {
if (lengths[symbol] > MAX_HUFFMAN_BITS) {
throw GZIPError{"Huffman code length out of range"};
}
// The fixed trees use lengths five to nine, the code length tree
// reads three bit values, and the dynamic trees copy code length
// symbols below sixteen, so no caller can supply a longer length
assert(lengths[symbol] <= MAX_HUFFMAN_BITS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Converting the out-of-range length check to assert removes the safety net in release builds: assert is compiled out under NDEBUG, so if any caller ever passes a length greater than MAX_HUFFMAN_BITS (15), count_[lengths[symbol]] becomes an out-of-bounds write into the 16-element count_ array instead of throwing a clean GZIPError as before. The current gzip callers do guarantee lengths ≤ 15, and HuffmanDecoder::build is a public method of this library header, so this is a latent robustness regression. Consider keeping the runtime validation (throw) so malformed or unexpected lengths are handled gracefully even in optimized builds, or document that the method now requires lengths ≤ MAX_HUFFMAN_BITS from all future callers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/gzip/huffman.h, line 44:

<comment>Converting the out-of-range length check to `assert` removes the safety net in release builds: `assert` is compiled out under `NDEBUG`, so if any caller ever passes a length greater than MAX_HUFFMAN_BITS (15), `count_[lengths[symbol]]` becomes an out-of-bounds write into the 16-element `count_` array instead of throwing a clean `GZIPError` as before. The current gzip callers do guarantee lengths ≤ 15, and `HuffmanDecoder::build` is a public method of this library header, so this is a latent robustness regression. Consider keeping the runtime validation (throw) so malformed or unexpected lengths are handled gracefully even in optimized builds, or document that the method now requires lengths ≤ MAX_HUFFMAN_BITS from all future callers.</comment>

<file context>
@@ -37,9 +38,10 @@ class HuffmanDecoder {
+      // The fixed trees use lengths five to nine, the code length tree
+      // reads three bit values, and the dynamic trees copy code length
+      // symbols below sixteen, so no caller can supply a longer length
+      assert(lengths[symbol] <= MAX_HUFFMAN_BITS);
       this->count_[lengths[symbol]]++;
     }
</file context>
Suggested change
assert(lengths[symbol] <= MAX_HUFFMAN_BITS);
if (lengths[symbol] > MAX_HUFFMAN_BITS) {
throw GZIPError{"Huffman code length out of range"};
}

this->count_[lengths[symbol]]++;
}

Expand Down
8 changes: 5 additions & 3 deletions src/core/http/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <sourcemeta/core/http_syntax.h>
#include <sourcemeta/core/text.h>

#include <cassert> // assert
#include <cstddef> // std::size_t
#include <cstdint> // std::uint8_t, std::uint16_t
#include <string_view> // std::string_view
Expand All @@ -29,11 +30,12 @@ inline auto http_media_specificity(const std::string_view range,
return 1;
}
const auto range_slash{range.find('/')};
const auto candidate_slash{candidate.find('/')};
if (range_slash == std::string_view::npos ||
candidate_slash == std::string_view::npos) {
if (range_slash == std::string_view::npos) {
return 0;
}
// Every caller validates its candidate media types upfront
const auto candidate_slash{candidate.find('/')};
assert(candidate_slash != std::string_view::npos);
if (range.size() - range_slash != 2 || range[range_slash + 1] != '*') {
return 0;
}
Expand Down
37 changes: 16 additions & 21 deletions src/core/jsonpath/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -460,10 +460,8 @@ class JSONPathParser {
// member-name-shorthand = name-first *name-char
auto parse_shorthand_name() -> JSONPath::SelectorName {
JSON::String name;
if (this->at_end()) {
this->fail();
}

// Every caller rejects a query that ends right before a shorthand name
assert(!this->at_end());
const auto first{static_cast<unsigned char>(this->peek())};
if (first == '_' || is_alpha(static_cast<char>(first))) {
name += static_cast<char>(first);
Expand Down Expand Up @@ -641,36 +639,33 @@ class JSONPathParser {
}

// comparison-op = "==" / "!=" / "<=" / ">=" / "<" / ">"
// Callers look ahead for a full comparison operator before parsing one, so
// the first character is one of the four operator openers, and an equals
// sign always follows an exclamation mark or another equals sign
auto parse_comparison_operator() -> JSONPath::FilterComparisonOperator {
const char character{this->peek()};
if (character == '=' || character == '!') {
this->position_ += 1;
if (this->at_end() || this->peek() != '=') {
this->fail();
}

assert(!this->at_end() && this->peek() == '=');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This refactor replaces the defensive this->fail() paths in parse_comparison_operator() and parse_shorthand_name() with assert(...). That is fine for validating internal invariants, but assert is compiled out under NDEBUG, so in release/production builds the parser no longer rejects malformed input on these paths. The current call sites in parse_comparison_or_test() do honor comparison_operator_ahead() first, so behavior today is unchanged, but the parser is a library that accepts external query strings and previously used these fail() calls to throw a precise JSONPathParseError. If a future caller (or a grammar change) reaches these helpers without the guard, a release build would read out of bounds via peek() or silently return a comparison operator instead of raising a parse error. Consider keeping explicit fail() guards for the user-input paths, or at least documenting the assert so the invariant is enforced in release too.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/jsonpath/parser.h, line 649:

<comment>This refactor replaces the defensive `this->fail()` paths in `parse_comparison_operator()` and `parse_shorthand_name()` with `assert(...)`. That is fine for validating internal invariants, but `assert` is compiled out under `NDEBUG`, so in release/production builds the parser no longer rejects malformed input on these paths. The current call sites in `parse_comparison_or_test()` do honor `comparison_operator_ahead()` first, so behavior today is unchanged, but the parser is a library that accepts external query strings and previously used these `fail()` calls to throw a precise `JSONPathParseError`. If a future caller (or a grammar change) reaches these helpers without the guard, a release build would read out of bounds via `peek()` or silently return a comparison operator instead of raising a parse error. Consider keeping explicit `fail()` guards for the user-input paths, or at least documenting the `assert` so the invariant is enforced in release too.</comment>

<file context>
@@ -641,36 +639,33 @@ class JSONPathParser {
-        this->fail();
-      }
-
+      assert(!this->at_end() && this->peek() == '=');
       this->position_ += 1;
       return character == '=' ? JSONPath::FilterComparisonOperator::Equal
</file context>
Suggested change
assert(!this->at_end() && this->peek() == '=');
if (this->at_end() || this->peek() != '=') {
this->fail();
}

this->position_ += 1;
return character == '=' ? JSONPath::FilterComparisonOperator::Equal
: JSONPath::FilterComparisonOperator::NotEqual;
}

if (character == '<' || character == '>') {
assert(character == '<' || character == '>');
this->position_ += 1;
const bool inclusive{!this->at_end() && this->peek() == '='};
if (inclusive) {
this->position_ += 1;
const bool inclusive{!this->at_end() && this->peek() == '='};
if (inclusive) {
this->position_ += 1;
}

if (character == '<') {
return inclusive ? JSONPath::FilterComparisonOperator::LessEqual
: JSONPath::FilterComparisonOperator::Less;
}
}

return inclusive ? JSONPath::FilterComparisonOperator::GreaterEqual
: JSONPath::FilterComparisonOperator::Greater;
if (character == '<') {
return inclusive ? JSONPath::FilterComparisonOperator::LessEqual
: JSONPath::FilterComparisonOperator::Less;
}

this->fail();
return inclusive ? JSONPath::FilterComparisonOperator::GreaterEqual
: JSONPath::FilterComparisonOperator::Greater;
}

auto parse_comparison_or_test() -> JSONPath::FilterExpression {
Expand Down
7 changes: 3 additions & 4 deletions src/core/uri/setters.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "escaping.h"
#include "normalize.h"

#include <cassert> // assert
#include <cstddef> // std::size_t
#include <optional> // std::optional
#include <string> // std::string
Expand All @@ -15,10 +16,8 @@ namespace {
auto apply_leading_slash_transform(std::optional<std::string> parsed_path,
const bool needs_leading_slash)
-> std::optional<std::string> {
if (!parsed_path.has_value()) {
return parsed_path;
}

// Every caller wraps a concrete string into the optional
assert(parsed_path.has_value());
const auto &path_value = parsed_path.value();

if (needs_leading_slash) {
Expand Down
3 changes: 3 additions & 0 deletions test/crypto/crypto_secure_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,15 @@ TEST(secure_string_from_a_pointer_and_a_length) {
const char content[]{"hunter2"};
const sourcemeta::core::SecureString secret{content, 7};
EXPECT_EQ(secret.size(), 7);
EXPECT_EQ(secret, "hunter2");
EXPECT_EQ(secret[0], 'h');
EXPECT_EQ(secret[6], '2');
}

TEST(secure_string_of_a_repeated_byte) {
const sourcemeta::core::SecureString secret{5, 'x'};
EXPECT_EQ(secret.size(), 5);
EXPECT_EQ(secret, "xxxxx");
EXPECT_EQ(secret[0], 'x');
EXPECT_EQ(secret[4], 'x');
}
Expand All @@ -157,6 +159,7 @@ TEST(secure_string_writes_a_byte_by_index) {
secret[0] = 'H';
EXPECT_EQ(secret[0], 'H');
EXPECT_EQ(secret.size(), 7);
EXPECT_EQ(secret, "Hunter2");
}

TEST(secure_allocator_instances_are_interchangeable) {
Expand Down
Loading
Loading