Skip to content

GH-54903: [C++][OSS-Fuzz fuzzing] Update csv fuzzer and corpora/dict preparation - #54912

Open
arthurscchan wants to merge 1 commit into
apache:mainfrom
arthurscchan:fuzz/csv-options
Open

arthurscchan wants to merge 1 commit into
apache:mainfrom
arthurscchan:fuzz/csv-options

Conversation

@arthurscchan

@arthurscchan arthurscchan commented Oct 21, 2026 •

Copy link
Copy Markdown

This PR updates the csv fuzzer with new corpora/dict, together with google/oss-fuzz#16332 to improve the fuzzer for project arrow. This PR is the 3rd PR aiming to improve the overall fuzzing coverage and effectiveness of the arrow project, mentioned in #54903.

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
@arthurscchan arthurscchan changed the title OSS-Fuzz: Update csv fuzzer and corpora/dict preparation GH-54903: [OSS-Fuzz fuzzing] Update csv fuzzer and corpora/dict preparation Oct 21, 2026
@github-actions github-actions Bot added the awaiting review Awaiting review label Oct 21, 2026
@arthurscchan arthurscchan changed the title GH-54903: [OSS-Fuzz fuzzing] Update csv fuzzer and corpora/dict preparation GH-54903: [C++][OSS-Fuzz fuzzing] Update csv fuzzer and corpora/dict preparation Oct 22, 2026
@pitrou

pitrou commented Oct 22, 2026

Copy link
Copy Markdown
Member

Thanks for the PR @arthurscchan . Is there any reason this doesn't reuse our standard PR description template?

@pitrou pitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for doing this. On the principle this is interesting, but I think there are number of problems with the current implementation.

# Add examples from Pandas test suite
find "${PANDAS_DIR}/" -name "*.csv" -exec cp --backup=numbered '{}' "${CORPUS_DIR}" \;
# Some Pandas CSV files are exact duplicates of each other under different
# names, and pack_corpus.py fails on duplicate hashes; keep only one copy.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, can pack_corpus.py be changed to not fail on duplicate hashes then? Perhaps it should just emit a warning instead of failing?

@@ -0,0 +1,60 @@
# Tokens for arrow-csv-fuzz: CSV dialect characters, null/bool spellings,
# numeric edge cases and timestamp formats understood by the Arrow CSV reader.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it's not clear to me how dictionaries are used by the fuzzing engine.

Are inputs restricted to a combination of tokens from the dictionaries? Or are random mutations still applied on top?

Comment thread cpp/src/arrow/csv/fuzz.cc
Comment on lines +62 to +63
opts.parse.delimiter = kDelimiters[header[0] % 5];
opts.parse.quoting = !(header[1] & 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, instead of all the manual bit-twiddling here, can we use a dedicated packed structure with bit-fields?

Can get inspiration from the Parquet encoding fuzzer:

constexpr auto kPackedEncodingHeaderSize = 64;
ARROW_PACKED_START(struct, PackedEncodingHeader) {
ARROW_PACKED_START(struct, Header) {
uint8_t source_encoding_id;
uint8_t roundtrip_encoding_id;
uint8_t type_id;
int32_t type_length;
int32_t num_values;
};
ARROW_PACKED_END
static_assert(sizeof(Header) == 3 * 1 + 2 * 4);
using Reserved = std::array<uint8_t, kPackedEncodingHeaderSize - sizeof(Header)>;
Header header;
Reserved reserved;
};
ARROW_PACKED_END

Comment thread cpp/src/arrow/csv/fuzz.cc
ConvertOptions convert;
};

FuzzCsvOptions MakeFuzzCsvOptions(const uint8_t* header) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just like in

::arrow::Result<FuzzEncodingHeader::ParseResult> FuzzEncodingHeader::Parse(
, it would be nicer for this function to take a std::span<const uint8_t> IMHO.

Comment thread cpp/src/arrow/csv/fuzz.cc
parse_options, convert_options));
ARROW_ASSIGN_OR_RAISE(
auto table_reader,
TableReader::Make(io_context, input, read_options, opts.parse, opts.convert));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

At this point, the input has been exhausted by the previous table reader call, so you'd just getting an empty table here?

Comment thread cpp/src/arrow/csv/fuzz.cc
return table->ValidateFull();
};
st &= table->ValidateFull();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note: if all three table reads are successful, we may want to add differential fuzzing, i.e. test that all three tables are equal (but there may be legitimate reasons why they are not?).

Comment on lines +181 to +182
// Small batch sizes exercise the writer's chunking and produce files that
// make the reader merge many batches.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure we care about exercising the writer here. We're just generating files for the reader.

And since those are CSV files, there is no inherent chunking in the file contents, so I don't understand what this is achieving.

Comment thread cpp/src/arrow/csv/fuzz.cc
Comment on lines +172 to +174
// Legacy behavior for inputs without a full header: the original
// hardcoded options. Small block sizes make chunking more likely to be
// exercised, as most legacy corpus entries are in the 4-10 kB range.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, the header is small (8 bytes), and most legacy inputs will actually go through the other code path, and get a non-sensical header as well as an unexpected non-header payload.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A possible workaround would be to have a magic number in front of the new header. If the magic number matches, parse the header, otherwise consider the input legacy.

ARROW_ASSIGN_OR_RAISE(auto sample_fn, dir_fn.Join(sample_name()));
std::cerr << sample_fn.ToString() << std::endl;
ARROW_ASSIGN_OR_RAISE(auto file, io::FileOutputStream::Open(sample_fn.ToString()));
RETURN_NOT_OK(file->Write(buffer));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should also be writing the serialized FuzzCsvOptions header, no?

See the Parquet encoding fuzzer for how this can be done.

@pitrou

pitrou commented Oct 22, 2026

Copy link
Copy Markdown
Member

cc @addisoncrump if you want a bit more of fuzzing-related fun :)

@github-actions github-actions Bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting committer review Awaiting committer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants