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
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ if(BUILD_TESTING)
loglens_enable_warnings(test_parser_properties)
add_test(NAME parser_properties COMMAND test_parser_properties)

add_executable(test_timestamp_boundaries tests/test_timestamp_boundaries.cpp)
target_link_libraries(test_timestamp_boundaries PRIVATE loglens_lib)
loglens_enable_warnings(test_timestamp_boundaries)
add_test(NAME timestamp_boundaries COMMAND test_timestamp_boundaries)

add_executable(test_detector tests/test_detector.cpp)
target_link_libraries(test_detector PRIVATE loglens_lib)
loglens_enable_warnings(test_detector)
Expand Down Expand Up @@ -159,4 +164,11 @@ if(LOGLENS_BUILD_FUZZERS)
-fno-omit-frame-pointer
)
target_link_options(fuzz_parser PRIVATE -fsanitize=fuzzer,address,undefined)

add_executable(fuzz_timestamp_day fuzz/timestamp_day_fuzz.cpp)
target_link_libraries(fuzz_timestamp_day PRIVATE loglens_fuzz_lib)
loglens_enable_warnings(fuzz_timestamp_day)
target_compile_options(fuzz_timestamp_day PRIVATE
-fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
target_link_options(fuzz_timestamp_day PRIVATE -fsanitize=fuzzer,address,undefined)
endif()
184 changes: 184 additions & 0 deletions FUZZING-OR-BOUNDARY-TEST-REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Executed boundary test report

Date: 2026-10-15. Scope: syslog integer day to calendar conversion only.
Result: **ACCEPT — confirmed defect with failing regression and validated fix**.

## Environment

Linux under WSL; GCC/G++ 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1), libstdc++,
CMake 3.28.3, Ninja 1.11.1, Python 3.12.3, jsonschema 4.10.3. Clang/libFuzzer
was initially unavailable. With explicit user authorization, the distribution
packages `clang` and `libclang-rt-dev` were installed, providing Ubuntu Clang
18.1.3 and compiler-rt/libFuzzer 18. The existing general parser fuzz harness
and CI smoke configuration are unchanged; a focused day-token target is added.

## Corpus and oracle

`tests/test_timestamp_boundaries.cpp` constructs the corpus deterministically;
there is no random seed, external input, or downloaded dataset. Each line uses
this synthetic template, with a substituted month/day:

```text
Mar 257 08:30:00 example-host sshd[4200]: Failed password for user-a from 203.0.113.10 port 50101 ssh2
```

The explicit 257 witness runs first. The full corpus contains:

| Cases | Inputs / purpose |
| ---: | --- |
| 1 | Minimal positive wrap witness: day 257 |
| 2,049 | March day integers -1024..1024 inclusive |
| 124 | Valid days 1..31 shifted by -65536, -256, 256, 65536; none may alias |
| 8 | INT_MAX, INT_MIN, both adjacent overflow strings, `1x`, bytes `31 00`, bytes `31 ff`, and 4,096 ASCII nines |
| 1 | `01` remains accepted as day 1 |
| 744 | All months, days 1..31, in leap year 2024 and common year 2026 |
| **2,927** | **Total parse_line cases, including intentional overlap** |

An additional three-line stream contains valid day 1, invalid day 257, valid day 2.
It must emit two events and one timestamp warning, preserve line numbers 1/3/2,
and account for all three lines. The oracle uses explicit integer domain and
month lengths; it never constructs a `chrono::day` from an adversarial value.
For accepted values it also checks exact timestamp, line number, and cleared
error text. Rejected values check category and reason. INT overflow/invalid
bytes remain `invalid day token`; representable out-of-domain integers become
`invalid calendar date`.

## Commands and observed baseline

Commands below run from the repository root in a Linux shell. Build directories
are local only. The non-PIE flags select a fixed executable layout for this
local GCC sanitizer build; they are not project defaults or a product change.

At baseline `e2937e8`, before adding tests or changing production code:

```sh
cmake -S . -B build-reliability -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DLOGLENS_WARNINGS_AS_ERRORS=ON \
'-DCMAKE_CXX_FLAGS=-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer -fno-pie' \
-DCMAKE_EXE_LINKER_FLAGS=-no-pie
cmake --build build-reliability -j 4
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
ctest --test-dir build-reliability --output-on-failure
```

Baseline: build succeeded; **7/7 CTest entries passed in 3.39 seconds**, no
ASan/UBSan findings. After adding only the new test and CMake registration:

```sh
cmake --build build-reliability --target test_timestamp_boundaries -j 4
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
ctest --test-dir build-reliability -R timestamp_boundaries --output-on-failure
```

Result: **0/1 passed**, CTest failure, `invalid day accepted at case 1`.
The test failed in 0.03 seconds (CTest total 0.05 seconds). This demonstrates
that the existing green suite did not detect invalid-day acceptance.

## Fixed implementation validation

After adding the pre-conversion 1..31 guard:

```sh
cmake --build build-reliability -j 4
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
ctest --test-dir build-reliability -R timestamp_boundaries -V
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
ctest --test-dir build-reliability --output-on-failure
```

Focused result: **2,927 cases and one three-line stream passed**, 0.07 seconds
(CTest total 0.10 seconds). Full result: **8/8 passed, 3.81 seconds**. No
ASan, leak, or UBSan findings were observed. All C++ targets in this build,
including the library and CLI, receive the sanitizer compile flags; Python
research tests run normally and are not C++ sanitizer coverage.

An additional non-sanitized optimized build uses:

```sh
cmake -S . -B build/reliability-release -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DLOGLENS_WARNINGS_AS_ERRORS=ON
cmake --build build/reliability-release -j 4
ctest --test-dir build/reliability-release --output-on-failure
git diff --check
```

Release build succeeded: **8/8 passed in 2.06 seconds**. Warnings-as-errors applies
to both builds. No dedicated formatter/type-check target is configured; the
compiler and test suite provide the applicable checks here.

## Focused libFuzzer execution

The new optional `fuzz_timestamp_day` target uses the existing instrumented
`loglens_fuzz_lib`. It substitutes arbitrary bytes into the day token of one
fixed March 2026 SSH failure line. Empty inputs and tokens containing ASCII
whitespace return early to keep envelope injection outside this target.
An independent integer-domain check requires acceptance exactly when the
complete token parses as an integer in 1..31, then checks the timestamp.
It shares `from_chars` with production, so it is not an independent lexical
oracle; its independent property is validation before narrowing. Rejections
must retain timestamp telemetry. The existing general parser fuzzer alone
would not detect this bug because an incorrectly accepted event still meets
its type/program invariants.

Three committed seeds contain exact ASCII bytes without newlines: `257`,
`-255`, `31`. Mutated corpus entries stay in an ignored build directory.
Installation (as root), followed by the build/run from the repository root:

```sh
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends clang libclang-rt-dev
clang++ --version
cmake -S . -B build/reliability-fuzz -G Ninja \
-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DBUILD_TESTING=OFF -DLOGLENS_BUILD_FUZZERS=ON \
-DLOGLENS_WARNINGS_AS_ERRORS=ON -DCMAKE_CXX_FLAGS=-fno-sanitize-recover=all
cmake --build build/reliability-fuzz --target fuzz_timestamp_day -j 4
mkdir -p build/reliability-fuzz/day-corpus
cp tests/fuzz/corpus/timestamp_day/* build/reliability-fuzz/day-corpus/
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
./build/reliability-fuzz/fuzz_timestamp_day -runs=10000 -seed=20261015 \
-max_len=4096 -timeout=2 -print_final_stats=1 build/reliability-fuzz/day-corpus
```

For a fresh replay, use a fresh build/corpus directory containing only those
three seeds. Mutation sequences and coverage counts can vary with toolchain.
The run count includes harness calls that skip empty/whitespace tokens; it is
not the number of distinct inputs or accepted parser calls. The maximum length
is a cap, not a claim that fuzzing exercised all sizes up to that length; the
deterministic test separately covers the 4,096-digit token.

Observed result: exit 0, **10,000 executed units**, no oracle failure, crash,
timeout, ASan, leak, or UBSan diagnostic. libFuzzer reported `cov: 227`,
`ft: 288`, final corpus 25 entries / 268 bytes, peak RSS 40 MB, and elapsed
`0 second(s)` at its whole-second resolution (not zero execution cost).
Its final growth limit was 80 bytes; this run did not reach the 4,096-byte cap.
These coverage counters are build-specific feedback, not source-line coverage
percentages or evidence of all paths being covered.

Final integration check also built both optional fuzz executables and reran the
Release suite after the CMake addition:

```sh
cmake --build build/reliability-fuzz --target fuzz_parser fuzz_timestamp_day -j 4
cmake --build build/reliability-release -j 4
ctest --test-dir build/reliability-release --output-on-failure
git diff --cached --check
```

Both fuzz targets built successfully; Release CTest passed **8/8 in 1.97 seconds**.
Diff whitespace checks passed. Final source/doc review and a scoped scan found
no private machine paths, personal identifiers, or credential patterns in the
new artifacts; only synthetic fixture identities are included.

## Interpretation

The production diff is a five-line guard/comment, with CTest registration,
deterministic tests, a focused fuzz oracle/corpus, and contract/evidence documentation. The new test is part
of ordinary CTest, so existing Linux/Windows CI definitions will discover it
when a future authorized push occurs; remote CI was not run here.

This is semantic regression evidence and bounded dynamic sanitizer/fuzz evidence,
not exhaustive memory safety, exhaustive fuzz coverage, or a performance benchmark.
No default detection algorithm, frozen experiment, source dataset, or existing
golden was changed. Fixtures use reserved documentation addresses and synthetic
identities. No push, PR, merge, tag, or release was performed.
84 changes: 84 additions & 0 deletions RELIABILITY-AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Reliability audit: syslog day conversion

Date: 2026-10-15. Baseline: `e2937e8`. Local branch:
`reliability/syslog-day-boundaries`. Decision: **ACCEPT**, completion criterion A.
This is a bounded source review and executed regression, not a complete security audit.

## Inventory and contracts

- C++20, extensions disabled; CMake >= 3.20, CTest, standard-library parser.
- Six baseline C++ test executables cover parsing, generated properties,
detectors, reports, CLI, and report contracts. An optional seventh CTest entry
runs the Python episode-candidate research tests.
- CLI input reaches `AuthLogParser::parse_file`, then `parse_stream` (`getline`),
then `parse_line`. Internal stages are timestamp/hostname, source envelope,
source-IP classification, program dispatch, and handler normalization.
- Configuration uses its own JSON cursor; CLI year conversion and parser integer
conversion use `from_chars`. CLI/config years are restricted to 1000..9999.
- `parse_line` returns an optional owning `Event`, resetting caller-supplied
diagnostics before parsing. Temporary `string_view` tokens borrow input;
event strings own their data. `parse_stream` accumulates events and warnings.
`parse_file` throws on open failure. Allocation failure/resource exhaustion
is not converted into a per-line warning. Stream I/O failure is a separate,
untested boundary in this increment.
- Existing deterministic properties include 512 arbitrary-byte inputs in both
modes, malformed IPv4 tokens, telemetry stability, and handler permutations.
- Existing `fuzz/parser_fuzz.cpp` invokes both modes with seven corpus seeds.
CMake instruments the fuzz library and driver with ASan/UBSan/libFuzzer.
CI defines a 2,000-run, 1,024-byte fuzz smoke job, Linux/Windows CTest jobs,
and C++ CodeQL analysis. These are configuration observations, not claims of
a newly executed remote CI campaign.
- `docs/adr/0001-episode-semantics-boundaries.md` keeps research candidate
algorithms distinct from production detection. No research fixtures, results,
candidate algorithms, finding identities, signal defaults, or report schema
are edited. Paper A1 is entirely excluded.

## Candidate assessment

Likelihood below means plausibility from inspected code, not measured prevalence.
Only the selected target received new experimental coverage.

| Surface | Likelihood | Potential impact | Reproducibility / testability | Decision |
| --- | --- | --- | --- | --- |
| Truncated/malformed auth lines | Common input; defect unconfirmed | Lost events or wrong warning | High; existing fixtures and byte properties | Defer broader prefix corpus |
| Encoding and embedded NUL | Plausible input; defect unconfirmed | Token confusion or output ambiguity | High; length-aware views and unsigned-char ctype casts inspected | Only day-token NUL/high-byte controls added |
| Oversized input | Plausible; no explicit getline/retention cap | Memory pressure from long lines or accumulated results | High for bounded cases; resource policy unspecified | Defer; one 4,096-digit day tested |
| Numeric conversion/narrowing | Confirmed for day conversion | Invalid input can emit an event on an incorrect date | Minimal `Mar 257` witness; deterministic oracle | **Select** |
| Lifetime/ownership | Low source-level signal; not cleared | Potential memory corruption if borrowing escapes | ASan feasible; events own strings | Run sanitizers, no separate lifetime redesign |
| Invalid timestamps | Confirmed day subcase; other cases unconfirmed | Evidence chronology and downstream counts | Independent month lengths and leap-year controls | Same selected conversion boundary |
| Sequential events | Parser config is immutable across calls; detector state is more complex | Recovery/accounting or grouping errors | Three-line parser recovery is deterministic | Verify recovery; leave detector method frozen |

## Confirmed defect and smallest fix

`build_timestamp` constructed `chrono::day` from an unchecked `int` and only
then called `year_month_day::ok()`. The tested libstdc++ representation narrows
the day before validation. A syntactically valid integer outside the day domain
can therefore become a valid date. The baseline accepted the `257` witness;
the new test failed immediately. No sanitizer diagnostic was necessary to
detect this semantic error, and no memory-corruption vulnerability is claimed.

The fix rejects values outside 1..31 before calendar construction. Existing
month/leap validation and error taxonomy remain in use. Valid-input detection
semantics are unchanged; previously accepted invalid day tokens now become
warnings and can no longer contribute events. Consumers that relied on this
invalid-input acceptance will observe different counts.

## Decision evidence and limits

- Rejected: relying only on ASan/UBSan. Defined narrowing can still violate a
semantic input contract; a domain oracle is required.
- Rejected: normalizing or clamping invalid days. That invents evidence dates.
- Rejected: changing all integer parsing or adding a global line-size policy.
Neither is needed for the demonstrated day defect.
- No goldens were updated. Existing report, CLI, detector, and research tests
pass without fixture changes; exact commands and results are in
[the execution report](FUZZING-OR-BOUNDARY-TEST-REPORT.md).
- This run covers Linux/GCC and Clang with libstdc++; Windows/MSVC and other standard libraries
were not executed locally. Finite clean sanitizer runs do not prove complete
memory safety or rule out other timestamp defects. Direct API year ranges,
timezone/clock lexical policy, and resource limits remain outside scope.
- Rollback: revert this increment; doing so restores invalid-day acceptance.
No data migration or research-artifact regeneration is involved.
- Suggested next increment: deterministic truncation tests around the source
envelope/program delimiter, including owning-event lifetime checks. Do not
implement automatically or expand the supported grammar.
5 changes: 5 additions & 0 deletions docs/parser-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ boundary.

Supported timezone tokens for `journalctl_short_full` are intentionally narrow: `UTC`, `GMT`, `Z`, and numeric offsets such as `+0000` or `+00:00`.

Day values must be within `1..31` before conversion to a calendar type;
month length and leap-year validity are checked afterward. Out-of-range days
produce `unknown_timestamp` / `invalid calendar date` warnings rather than
wrapping into valid dates. See [the boundary test report](../FUZZING-OR-BOUNDARY-TEST-REPORT.md).

## Recognized event families

The parser currently recognizes common authentication evidence from:
Expand Down
49 changes: 49 additions & 0 deletions fuzz/timestamp_day_fuzz.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "parser.hpp"

#include <charconv>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <string_view>

extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
// The corpus mutates one token, not the surrounding source envelope.
if (size == 0) {
return 0;
}
const std::string_view token(reinterpret_cast<const char*>(data), size);
if (token.find_first_of(" \t\r\n\f\v") != std::string_view::npos) {
return 0;
}
int day = 0;
const auto converted = std::from_chars(token.data(), token.data() + token.size(), day);
const bool valid = converted.ec == std::errc{}
&& converted.ptr == token.data() + token.size() && day >= 1 && day <= 31;
const std::string line = "Mar " + std::string(token)
+ " 08:30:00 example-host sshd[4200]: Failed password for user-a"
" from 203.0.113.10 port 50101 ssh2";
const loglens::AuthLogParser parser({loglens::InputMode::SyslogLegacy, 2026});
std::string reason;
auto category = loglens::ParserFailureCategory::MalformedSourceIp;
const auto event = parser.parse_line(line, 1, &reason, &category);
if (event.has_value() != valid) {
std::abort();
}
if (!valid) {
if (reason.empty() || category != loglens::ParserFailureCategory::UnknownTimestamp) {
std::abort();
}
return 0;
}
// Only range-checked values reach chrono in the oracle.
const auto expected = std::chrono::sys_days{
std::chrono::year{2026} / std::chrono::March
/ std::chrono::day{static_cast<unsigned>(day)}}
+ std::chrono::hours{8} + std::chrono::minutes{30};
if (event->timestamp != expected || !reason.empty()) {
std::abort();
}
return 0;
}
5 changes: 5 additions & 0 deletions src/parser/timestamp_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ std::optional<std::chrono::sys_seconds> build_timestamp(int year_value,
std::chrono::minutes offset = std::chrono::minutes{0}) {
using namespace std::chrono;

// Validate before chrono::day can narrow an out-of-range integer.
if (day_value < 1 || day_value > 31) {
return std::nullopt;
}

const year_month_day date{year{year_value}, month{month_index}, day{static_cast<unsigned>(day_value)}};
if (!date.ok()) {
return std::nullopt;
Expand Down
6 changes: 6 additions & 0 deletions tests/fuzz/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Parser fuzz harness

For the focused day-token oracle, build the optional `fuzz_timestamp_day`
target using the same configuration. Its three seeds are in
`tests/fuzz/corpus/timestamp_day`; copy them into a writable build-directory
corpus before running. See [the executed boundary/fuzz report](../../FUZZING-OR-BOUNDARY-TEST-REPORT.md)
for exact commands, the confirmed day-narrowing regression, and coverage limits.

The parser fuzz target is optional and requires Clang with libFuzzer support.
It exercises both supported input modes and treats these result invariants as
crash conditions:
Expand Down
1 change: 1 addition & 0 deletions tests/fuzz/corpus/timestamp_day/valid_day
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
31
1 change: 1 addition & 0 deletions tests/fuzz/corpus/timestamp_day/wrapped_negative
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-255
1 change: 1 addition & 0 deletions tests/fuzz/corpus/timestamp_day/wrapped_positive
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
257
Loading
Loading