Skip to content

fuzz-tests: Add a test for codex32 operations #8390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

Chand-ra
Copy link

@Chand-ra Chand-ra commented Jul 3, 2025

Add a test for codex32_encode() and codex32_secret_decode() defined in common/codex32.{c, h}.

Checklist

Before submitting the PR, ensure the following tasks are completed. If an item is not applicable to your PR, please mark it as checked:

  • The changelog has been updated in the relevant commit(s) according to the guidelines.
  • Tests have been added or modified to reflect the changes.
  • Documentation has been reviewed and updated as needed.
  • Related issues have been listed and linked, including any that this PR closes.

CC: @morehouse

Comment on lines 18 to 25
static const char *valid_vectors[] = {
"ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw",
"MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM",
"ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln",
"ms10leetsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nlnve25gvezzyqqtum9pgv99ycma",
"MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXV"
"CEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK"
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think it's cleaner to put seeds in the corpus directory instead of hard-coding them into the fuzz target.

I'd prefer to just put one seed here, or even get rid of initial_input entirely and just return 0 instead for simplicity.

Comment on lines 62 to 102
if (streq(parts->hrp, "ms"))
parts->hrp = "MS";
else
parts->hrp = "ms";
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to limit the HRP to "MS" or "ms"? Allowing other mutations may trigger new coverage.

Comment on lines 67 to 106
case 1: /* Mutate threshold (0-9) */
parts->threshold = rand() % 10;
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to limit threshold here? Allowing other values may trigger new coverage.

Comment on lines 71 to 72
for (int i = 0; i < 4; i++)
parts->id[i] = 'A' + (rand() % 26);
Copy link
Contributor

Choose a reason for hiding this comment

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

Valid IDs have lowercase bech32 characters only. If the goal is to make a valid ID, we should be using those. Otherwise maybe we should leave it unconstrained?

parts->id[4] = '\0';
break;
case 3: /* Mutate share index (valid char) */
parts->share_idx = "abcdefghijklmnopqrstuvwxyzABCDEF"[rand() % 32];
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems codex32_secret_encode overwrites the share_idx with s anyway, so this mutation is currently pointless.

Comment on lines 79 to 122
if (tal_bytelen(parts->payload) > 0) {
size_t mutate_len = 1 + rand() % tal_bytelen(parts->payload);
LLVMFuzzerMutate((u8 *) parts->payload, mutate_len,
tal_bytelen(parts->payload));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We should let LLVMFuzzerMutate decide how many bytes to mutate, and we should allow it to increase or decrease the payload size.

Suggested change
if (tal_bytelen(parts->payload) > 0) {
size_t mutate_len = 1 + rand() % tal_bytelen(parts->payload);
LLVMFuzzerMutate((u8 *) parts->payload, mutate_len,
tal_bytelen(parts->payload));
}
size_t cur_size = tal_bytelen(parts->payload);
tal_resize(parts->payload, max_size);
size_t new_size = LLVMFuzzerMutate((u8 *) parts->payload, cur_size, max_size);
tal_resize(parse->payload, new_size);

Comment on lines 122 to 216
switch(rand() % 5) {
case 0:
child->hrp = p2->hrp;
break;
case 1:
child->threshold = p2->threshold;
break;
case 2:
memcpy(child->id, p2->id, 5);
break;
case 3:
child->share_idx = p2->share_idx;
break;
case 4: /* Payload crossover */
child->payload = tal_arr(child, u8, tal_bytelen(p2->payload));
memcpy((u8 *) child->payload, p2->payload, tal_bytelen(p2->payload));
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

The idea behind the crossover mutator is to combine pieces of each input into a new input (like genetic crossover). Using the cross_over helper function from libfuzz.h here would greatly strengthen this mutator.

@Chand-ra
Copy link
Author

Chand-ra commented Jul 8, 2025

Hey @morehouse, I was working on incorporating the feedback above but the resulting target fails with an out-of-bounds error when executed on the current corpus. Executing the resulting crash file however, fails to reproduce the crash:

chand@Ubuntu:~/lightning$ tests/fuzz/fuzz-codex32 local_corpora/fuzz-codex32/

INFO: found LLVMFuzzerCustomMutator (0x5f37a5e196d0). Disabling -len_control by default.
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 202808252
INFO: Loaded 1 modules   (35545 inline 8-bit counters): 35545 [0x5f37a621f610, 0x5f37a62280e9), 
INFO: Loaded 1 PC tables (35545 PCs): 35545 [0x5f37a62280f0,0x5f37a62b2e80), 
INFO:      117 files found in local_corpora/fuzz-codex32/
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: seed corpus: files: 117 min: 10b max: 540b total: 14861b rss: 42Mb
#118	INITED cov: 252 ft: 542 corp: 41/4581b exec/s: 0 rss: 46Mb
#119	NEW    cov: 252 ft: 585 corp: 42/4708b lim: 4096 exec/s: 0 rss: 47Mb L: 127/262 MS: 1 CustomCrossOver-
#120	NEW    cov: 252 ft: 603 corp: 43/4809b lim: 4096 exec/s: 0 rss: 47Mb L: 101/262 MS: 2 InsertByte-Custom-
#131	NEW    cov: 252 ft: 604 corp: 44/4895b lim: 4096 exec/s: 0 rss: 47Mb L: 86/262 MS: 1 CustomCrossOver-
#157	NEW    cov: 252 ft: 618 corp: 45/5022b lim: 4096 exec/s: 0 rss: 47Mb L: 127/262 MS: 1 CustomCrossOver-
#194	NEW    cov: 252 ft: 619 corp: 46/5056b lim: 4096 exec/s: 0 rss: 48Mb L: 34/262 MS: 3 CMP-Custom-CustomCrossOver- DE: "\377\377\377\377"-
common/codex32.c:119:28: runtime error: index 242 out of bounds for type 'const uint8_t[32]' (aka 'const unsigned char[32]')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior common/codex32.c:119:28 
MS: 1 ShuffleBytes-; base unit: c00e8766131642fffb333d9c1e3f9170c31c1b1d

artifact_prefix='./'; Test unit written to ./crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
Base64: 

chand@Ubuntu:~/lightning$ tests/fuzz/fuzz-codex32 crash-da39a3ee5e6b4b0d3255bfef95601890afd80709

INFO: found LLVMFuzzerCustomMutator (0x601ae86816d0). Disabling -len_control by default.
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 4227437988
INFO: Loaded 1 modules   (35545 inline 8-bit counters): 35545 [0x601ae8a87610, 0x601ae8a900e9), 
INFO: Loaded 1 PC tables (35545 PCs): 35545 [0x601ae8a900f0,0x601ae8b1ae80), 
tests/fuzz/fuzz-codex32: Running 1 inputs 1 time(s) each.
Running: crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
Executed crash-da39a3ee5e6b4b0d3255bfef95601890afd80709 in 5 ms
***
*** NOTE: fuzzing was not performed, you have only
***       executed the target code on a fixed set of inputs.
***

chand@Ubuntu:~/lightning$

Is this an actual bug or something wrong with the target? Any ideas on how I can investigate this further?

@morehouse
Copy link
Contributor

Since it doesn't reproduce, it's likely a bug in one of your custom mutators. Check the line of code that UBSan is pointing to and go from there.

@Chand-ra
Copy link
Author

Chand-ra commented Jul 9, 2025

The test works without breaking now.

Comment on lines 91 to 93
struct codex32 *mutable_parts = codex32_dup(tmpctx, parts);
tal_free(parts);
parts = mutable_parts;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the copy needed, especially since we just discard the original anyway?

for (int i = 0; i < 4; i++)
parts->id[i] = rand();
parts->id[4] = '\0';
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should prefer LLVMFuzzerMutate here.

Suggested change
for (int i = 0; i < 4; i++)
parts->id[i] = rand();
parts->id[4] = '\0';
break;
size_t id_len = sizeof(parts->id) - 1;
LLVMFuzzerMutate(parts->id, id_len, id_len);
parts->id[id_len] = '\0';
break;

case 3: /* Mutate payload */
{
size_t cur_size = tal_bytelen(parts->payload);
size_t max_payload_size = max_size;
Copy link
Contributor

Choose a reason for hiding this comment

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

max_payload_size is unnecessary; we can just use max_size directly.

{
size_t cur_size = tal_bytelen(parts->payload);
size_t max_payload_size = max_size;
u8 *new_payload = tal_arr(parts, u8, max_payload_size);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we just resize the original parts->payload, rather than managing an extra pointer?

parts->threshold, parts->payload,
tal_bytelen(parts->payload), &reencoded);
if (!err) {
size_t len = strlen(reencoded);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we can use tal_bytelen here instead, that should be more efficient.

Comment on lines 129 to 133
/* Apply mutation */
size_t new_size = LLVMFuzzerMutate(new_payload,
cur_size < max_payload_size ?
cur_size : max_payload_size,
max_payload_size);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure why we would need to take the min here.

Suggested change
/* Apply mutation */
size_t new_size = LLVMFuzzerMutate(new_payload,
cur_size < max_payload_size ?
cur_size : max_payload_size,
max_payload_size);
/* Apply mutation */
size_t new_size = LLVMFuzzerMutate(new_payload,
cur_size,
max_payload_size);

Comment on lines 200 to 204
u8 new_id[5];
cross_over((const u8 *)p1->id, 4, (const u8 *)p2->id, 4,
new_id, 4, rand());
new_id[4] = '\0';
memcpy(child->id, new_id, 4);
Copy link
Contributor

Choose a reason for hiding this comment

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

A few issues with this code.

  1. we fail to copy the null byte
  2. we can make it simpler by mutating child->id directly.
Suggested change
u8 new_id[5];
cross_over((const u8 *)p1->id, 4, (const u8 *)p2->id, 4,
new_id, 4, rand());
new_id[4] = '\0';
memcpy(child->id, new_id, 4);
size_t id_len = sizeof(p1->id) - 1;
cross_over((const u8 *)p1->id, id_len, (const u8 *)p2->id, id_len,
child->id, id_len, rand());
child->id[id_len] = '\0';

Comment on lines 212 to 217
u8 *new_payload = tal_arr(child, u8, max_out_size);
size_t new_payload_len = cross_over(p1->payload, p1_len,
p2->payload, p2_len,
new_payload, max_out_size, rand());
tal_free(child->payload);
child->payload = new_payload;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not resize child->payload for simplicity?

Suggested change
u8 *new_payload = tal_arr(child, u8, max_out_size);
size_t new_payload_len = cross_over(p1->payload, p1_len,
p2->payload, p2_len,
new_payload, max_out_size, rand());
tal_free(child->payload);
child->payload = new_payload;
tal_resize(&child->payload, u8, max_out_size);
size_t new_payload_len = cross_over(p1->payload, p1_len,
p2->payload, p2_len,
child->payload, max_out_size, rand());

if (rand() % 2)
child->threshold = p2->threshold;
if (rand() % 2)
memcpy(child->id, p2->id, 5);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit

Suggested change
memcpy(child->id, p2->id, 5);
memcpy(child->id, p2->id, sizeof(p2->id));

}
break;

case 4: /* Random combination */
Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to do a random combination, perhaps we should create functions for the above crossover cases, so we can reuse them here.

Copy link
Author

Choose a reason for hiding this comment

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

I think that would make the test more complicated without the added benefit to match, perhaps we should get rid of this case altogether?

Chandra Pratap added 2 commits July 11, 2025 07:20
Changelog-None: Add a test for `codex32_encode()` and
`codex32_secret_decode()` defined in `common/codex32.{c, h}`.
Add a minimal input set as a seed corpus for the newly introduced
test. This leads to discovery of interesting code paths faster.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants