Skip to content

Commit 534adbc

Browse files
fuzz: introduce robust fuzzing suite and harden Surface and Canvas against crash bugs
- Add dom_layout_fuzzer, canvas_fuzzer, utf8_fuzzer, and color_fuzzer. - Enable ASan & UBSan checks for fuzzer executables in CMake. - Fix to_wstring template resolution compilation error with std::string. - Safeguard Surface and Canvas initialization logic against negative dimensions. - Add comprehensive regression tests for negative dimensions and to_wstring. - Add CHANGELOG.md entry detailing the new fuzzing suite. - Clean up include statements and formatting using iwyu and format tools.
1 parent cd0b512 commit 534adbc

20 files changed

Lines changed: 631 additions & 44 deletions

cmake/ftxui_fuzzer.cmake

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ function(fuzz source)
1212
)
1313
target_include_directories(${name} PRIVATE src)
1414
target_link_libraries(${name} PRIVATE component)
15-
target_compile_options(${name} PRIVATE -fsanitize=fuzzer,address)
16-
target_link_libraries(${name} PRIVATE -fsanitize=fuzzer,address)
15+
target_compile_options(${name} PRIVATE -fsanitize=fuzzer,address,undefined)
16+
target_link_libraries(${name} PRIVATE -fsanitize=fuzzer,address,undefined)
1717
target_compile_features(${name} PRIVATE cxx_std_17)
1818
endfunction(fuzz)
1919

2020
fuzz(terminal_input_parser_test_fuzzer)
2121
fuzz(component_fuzzer)
22+
fuzz(dom_layout_fuzzer)
23+
fuzz(canvas_fuzzer)
24+
fuzz(utf8_fuzzer)
25+
fuzz(color_fuzzer)

include/ftxui/screen/string.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ template <typename T>
1818
std::wstring to_wstring(T s) {
1919
return to_wstring(std::string_view(std::to_string(s)));
2020
}
21+
inline std::wstring to_wstring(const std::string& s) {
22+
return to_wstring(std::string_view(s));
23+
}
2124
template <>
2225
inline std::wstring to_wstring(const char* s) {
2326
return to_wstring(std::string_view(s));

include/ftxui/util/export.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
// |FTXUI_MACRO_CONDITIONAL_COMMA_()| above to implement conditional macro
8080
// expansion.
8181
#define FTXUI_MACRO_SELECT_THIRD_ARGUMENT_(...) \
82-
FTXUI_MACRO_EXPAND( \
82+
FTXUI_MACRO_EXPAND( \
8383
FTXUI_MACRO_SELECT_THIRD_ARGUMENT_IMPL_(__VA_ARGS__, dummy))
8484
#define FTXUI_MACRO_SELECT_THIRD_ARGUMENT_IMPL_(a, b, c, ...) c
8585

src/ftxui/component/app.cpp

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#include <algorithm> // for any_of, copy, max, min
55
#include <array> // for array
66
#include <atomic>
7-
#include <map>
87
#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
98
#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
109
#include <cstdint>
@@ -15,6 +14,7 @@
1514
#include <functional> // for function
1615
#include <initializer_list> // for initializer_list
1716
#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
17+
#include <map>
1818
#include <memory>
1919
#include <stack> // for stack
2020
#include <string>
@@ -404,8 +404,7 @@ int g_tty_fd = -1;
404404
void RestoreSignalHandlerAndRaise(int signal) {
405405
#if defined(_WIN32)
406406
auto it = g_old_signal_handlers.find(signal);
407-
auto old_handler =
408-
(it != g_old_signal_handlers.end()) ? it->second : SIG_DFL;
407+
auto old_handler = (it != g_old_signal_handlers.end()) ? it->second : SIG_DFL;
409408
std::signal(signal, old_handler);
410409
#else
411410
auto it = g_old_sigactions.find(signal);
@@ -543,8 +542,7 @@ void InstallSignalHandler(int sig) {
543542
struct sigaction old_sa;
544543
sigaction(sig, &sa, &old_sa);
545544
g_old_sigactions[sig] = old_sa;
546-
on_exit_functions.emplace(
547-
[=] { sigaction(sig, &old_sa, nullptr); });
545+
on_exit_functions.emplace([=] { sigaction(sig, &old_sa, nullptr); });
548546
#endif
549547
}
550548

@@ -1054,8 +1052,8 @@ void App::Internal::Draw(Component component) {
10541052
if (resized) {
10551053
public_->dimx_ = dimx;
10561054
public_->dimy_ = dimy;
1057-
public_->cells_ =
1058-
std::vector<Cell>(static_cast<size_t>(dimx) * static_cast<size_t>(dimy));
1055+
public_->cells_ = std::vector<Cell>(static_cast<size_t>(dimx) *
1056+
static_cast<size_t>(dimy));
10591057
Cursor cursor = public_->cursor_;
10601058
cursor.x = dimx - 1;
10611059
cursor.y = dimy - 1;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright 2026 Arthur Sonzogni. All rights reserved.
2+
// Use of this source code is governed by the MIT license that can be found in
3+
// the LICENSE file.
4+
#include <cstddef>
5+
#include <string>
6+
#include <vector>
7+
#include "ftxui/dom/canvas.hpp"
8+
9+
using namespace ftxui;
10+
11+
namespace {
12+
13+
int GeneratorInt(const char*& data, size_t& size) {
14+
if (size == 0) {
15+
return 0;
16+
}
17+
auto out = int(data[0]);
18+
data++;
19+
size--;
20+
return out;
21+
}
22+
23+
bool GeneratorBool(const char*& data, size_t& size) {
24+
if (size == 0) {
25+
return false;
26+
}
27+
auto out = bool(data[0] % 2);
28+
data++;
29+
size--;
30+
return out;
31+
}
32+
33+
std::string GeneratorString(const char*& data, size_t& size) {
34+
int index = 0;
35+
while (index < size && data[index]) {
36+
++index;
37+
}
38+
auto out = std::string(data, data + index);
39+
data += index;
40+
size -= index;
41+
return out;
42+
}
43+
44+
} // namespace
45+
46+
extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
47+
if (size < 5) {
48+
return 0;
49+
}
50+
51+
// Read canvas width and height
52+
int width = GeneratorInt(data, size);
53+
int height = GeneratorInt(data, size);
54+
55+
// Keep dimensions bounded but allow some small, zero, negative or moderately
56+
// large values
57+
width = (width % 100) - 10;
58+
height = (height % 100) - 10;
59+
60+
Canvas canvas(width, height);
61+
62+
// Perform multiple drawing actions while data is available
63+
while (size > 2) {
64+
int action = GeneratorInt(data, size) % 15;
65+
switch (action) {
66+
case 0: {
67+
int x = GeneratorInt(data, size);
68+
int y = GeneratorInt(data, size);
69+
canvas.DrawPointOn(x, y);
70+
break;
71+
}
72+
case 1: {
73+
int x = GeneratorInt(data, size);
74+
int y = GeneratorInt(data, size);
75+
canvas.DrawPointOff(x, y);
76+
break;
77+
}
78+
case 2: {
79+
int x = GeneratorInt(data, size);
80+
int y = GeneratorInt(data, size);
81+
canvas.DrawPointToggle(x, y);
82+
break;
83+
}
84+
case 3: {
85+
int x1 = GeneratorInt(data, size);
86+
int y1 = GeneratorInt(data, size);
87+
int x2 = GeneratorInt(data, size);
88+
int y2 = GeneratorInt(data, size);
89+
canvas.DrawPointLine(x1, y1, x2, y2);
90+
break;
91+
}
92+
case 4: {
93+
int x = GeneratorInt(data, size);
94+
int y = GeneratorInt(data, size);
95+
int r = GeneratorInt(data, size) % 30;
96+
canvas.DrawPointCircle(x, y, r);
97+
break;
98+
}
99+
case 5: {
100+
int x = GeneratorInt(data, size);
101+
int y = GeneratorInt(data, size);
102+
int r = GeneratorInt(data, size) % 30;
103+
canvas.DrawPointCircleFilled(x, y, r);
104+
break;
105+
}
106+
case 6: {
107+
int x = GeneratorInt(data, size);
108+
int y = GeneratorInt(data, size);
109+
int r1 = GeneratorInt(data, size) % 30;
110+
int r2 = GeneratorInt(data, size) % 30;
111+
canvas.DrawPointEllipse(x, y, r1, r2);
112+
break;
113+
}
114+
case 7: {
115+
int x = GeneratorInt(data, size);
116+
int y = GeneratorInt(data, size);
117+
int r1 = GeneratorInt(data, size) % 30;
118+
int r2 = GeneratorInt(data, size) % 30;
119+
canvas.DrawPointEllipseFilled(x, y, r1, r2);
120+
break;
121+
}
122+
case 8: {
123+
int x = GeneratorInt(data, size);
124+
int y = GeneratorInt(data, size);
125+
canvas.DrawBlockOn(x, y);
126+
break;
127+
}
128+
case 9: {
129+
int x1 = GeneratorInt(data, size);
130+
int y1 = GeneratorInt(data, size);
131+
int x2 = GeneratorInt(data, size);
132+
int y2 = GeneratorInt(data, size);
133+
canvas.DrawBlockLine(x1, y1, x2, y2);
134+
break;
135+
}
136+
case 10: {
137+
int x = GeneratorInt(data, size);
138+
int y = GeneratorInt(data, size);
139+
int r = GeneratorInt(data, size) % 30;
140+
canvas.DrawBlockCircle(x, y, r);
141+
break;
142+
}
143+
case 11: {
144+
int x = GeneratorInt(data, size);
145+
int y = GeneratorInt(data, size);
146+
int r = GeneratorInt(data, size) % 30;
147+
canvas.DrawBlockCircleFilled(x, y, r);
148+
break;
149+
}
150+
case 12: {
151+
int x = GeneratorInt(data, size);
152+
int y = GeneratorInt(data, size);
153+
std::string s = GeneratorString(data, size);
154+
canvas.DrawText(x, y, s);
155+
break;
156+
}
157+
case 13: {
158+
int x = GeneratorInt(data, size);
159+
int y = GeneratorInt(data, size);
160+
canvas.GetCell(x, y);
161+
break;
162+
}
163+
case 14: {
164+
int x = GeneratorInt(data, size);
165+
int y = GeneratorInt(data, size);
166+
bool val = GeneratorBool(data, size);
167+
canvas.DrawPoint(x, y, val);
168+
break;
169+
}
170+
}
171+
}
172+
173+
return 0;
174+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2026 Arthur Sonzogni. All rights reserved.
2+
// Use of this source code is governed by the MIT license that can be found in
3+
// the LICENSE file.
4+
#include <cstddef>
5+
#include <cstdint>
6+
#include <string>
7+
#include "ftxui/screen/color.hpp"
8+
#include "ftxui/screen/terminal.hpp"
9+
10+
using namespace ftxui;
11+
12+
namespace {
13+
14+
uint8_t GeneratorByte(const char*& data, size_t& size) {
15+
if (size == 0) {
16+
return 0;
17+
}
18+
uint8_t out = static_cast<uint8_t>(data[0]);
19+
data++;
20+
size--;
21+
return out;
22+
}
23+
24+
float GeneratorFloat(const char*& data, size_t& size) {
25+
if (size == 0) {
26+
return 0.0f;
27+
}
28+
float out = float(static_cast<uint8_t>(data[0])) / 255.0f;
29+
data++;
30+
size--;
31+
return out;
32+
}
33+
34+
Color GeneratorColor(const char*& data, size_t& size) {
35+
if (size < 4) {
36+
return Color();
37+
}
38+
int type = GeneratorByte(data, size) % 5;
39+
switch (type) {
40+
case 0:
41+
return Color(
42+
static_cast<Color::Palette16>(GeneratorByte(data, size) % 16));
43+
case 1:
44+
return Color(static_cast<Color::Palette256>(GeneratorByte(data, size)));
45+
case 2: {
46+
uint8_t r = GeneratorByte(data, size);
47+
uint8_t g = GeneratorByte(data, size);
48+
uint8_t b = GeneratorByte(data, size);
49+
return Color::RGB(r, g, b);
50+
}
51+
case 3: {
52+
uint8_t h = GeneratorByte(data, size);
53+
uint8_t s = GeneratorByte(data, size);
54+
uint8_t v = GeneratorByte(data, size);
55+
return Color::HSV(h, s, v);
56+
}
57+
case 4: {
58+
uint8_t r = GeneratorByte(data, size);
59+
uint8_t g = GeneratorByte(data, size);
60+
uint8_t b = GeneratorByte(data, size);
61+
uint8_t a = GeneratorByte(data, size);
62+
return Color::RGBA(r, g, b, a);
63+
}
64+
default:
65+
return Color();
66+
}
67+
}
68+
69+
} // namespace
70+
71+
extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
72+
if (size < 5) {
73+
return 0;
74+
}
75+
76+
// Read terminal color support
77+
int support = GeneratorByte(data, size) % 4;
78+
switch (support) {
79+
case 0:
80+
Terminal::SetColorSupport(Terminal::Color::Palette1);
81+
break;
82+
case 1:
83+
Terminal::SetColorSupport(Terminal::Color::Palette16);
84+
break;
85+
case 2:
86+
Terminal::SetColorSupport(Terminal::Color::Palette256);
87+
break;
88+
case 3:
89+
Terminal::SetColorSupport(Terminal::Color::TrueColor);
90+
break;
91+
}
92+
93+
while (size > 1) {
94+
int op = GeneratorByte(data, size) % 4;
95+
switch (op) {
96+
case 0: {
97+
Color c = GeneratorColor(data, size);
98+
bool is_background = GeneratorByte(data, size) % 2;
99+
c.Print(is_background);
100+
break;
101+
}
102+
case 1: {
103+
Color c1 = GeneratorColor(data, size);
104+
Color c2 = GeneratorColor(data, size);
105+
float t = GeneratorFloat(data, size);
106+
Color result = Color::Interpolate(t, c1, c2);
107+
result.Print(true);
108+
result.Print(false);
109+
break;
110+
}
111+
case 2: {
112+
Color c1 = GeneratorColor(data, size);
113+
Color c2 = GeneratorColor(data, size);
114+
Color result = Color::Blend(c1, c2);
115+
result.Print(true);
116+
result.Print(false);
117+
break;
118+
}
119+
case 3: {
120+
uint8_t h = GeneratorByte(data, size);
121+
uint8_t s = GeneratorByte(data, size);
122+
uint8_t v = GeneratorByte(data, size);
123+
uint8_t a = GeneratorByte(data, size);
124+
Color result = Color::HSVA(h, s, v, a);
125+
result.Print(true);
126+
break;
127+
}
128+
}
129+
}
130+
131+
return 0;
132+
}

src/ftxui/component/component.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ Element ComponentBase::OnRender() {
171171
/// @return True when the event has been handled.
172172
/// The default implementation called OnEvent on every child until one return
173173
/// true. If none returns true, return false.
174-
bool ComponentBase::OnEvent(Event event) { // NOLINT
174+
bool ComponentBase::OnEvent(Event event) { // NOLINT
175175
for (Component& child : impl_->children) { // NOLINT
176176
if (child->OnEvent(event)) {
177177
return true;

0 commit comments

Comments
 (0)