Skip to content

Commit c0a3c9f

Browse files
committed
feat(test): add fuzzing test with libFuzzer
1 parent 44a40d1 commit c0a3c9f

File tree

4 files changed

+200
-1
lines changed

4 files changed

+200
-1
lines changed

.github/workflows/fuzzing.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Fuzzing
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
schedule:
11+
- cron: '0 0 * * *'
12+
workflow_dispatch:
13+
14+
jobs:
15+
fuzz_test:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 20
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Install build tools
22+
run: |
23+
sudo apt-get update
24+
sudo apt-get install -y clang llvm cmake ninja-build
25+
26+
- name: Configure fuzz target
27+
run: |
28+
cmake -S . -B build-fuzz -G Ninja \
29+
-DCTSHELL_ENABLE_FUZZING=ON \
30+
-DCONFIG_CTSHELL_PORT_POSIX=ON \
31+
-DCMAKE_C_COMPILER=clang
32+
33+
- name: Build fuzz target
34+
run: cmake --build build-fuzz --target ctshell_fuzz
35+
36+
- name: Run fuzz and collect profile
37+
run: |
38+
LLVM_PROFILE_FILE="ctshell.profraw" ./build-fuzz/ctshell_fuzz \
39+
-max_total_time=599
40+
41+
- name: Generate coverage report
42+
run: |
43+
llvm-profdata merge -sparse ctshell.profraw -o ctshell.profdata
44+
llvm-cov show ./build-fuzz/ctshell_fuzz \
45+
-instr-profile=ctshell.profdata \
46+
-format=html \
47+
-output-dir=coverage_report
48+
49+
- name: Upload coverage artifact
50+
uses: actions/upload-artifact@v4
51+
with:
52+
name: fuzz-coverage-report.zip
53+
path: coverage_report

CMakeLists.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ set(ctshell_incs
88

99
set(CTSHELL_DEFINITIONS "")
1010

11+
option(CTSHELL_ENABLE_FUZZING "Enable libFuzzer based fuzz testing" OFF)
12+
1113
if(ESP_PLATFORM)
1214
set(CONFIG_CTSHELL_PORT_ESP32 1)
1315
endif()
@@ -64,4 +66,32 @@ if(ESP_PLATFORM)
6466
target_link_libraries(${COMPONENT_LIB} INTERFACE "-T ${CMAKE_CURRENT_SOURCE_DIR}/port/esp32/ctshell_cmd.ld")
6567
else()
6668
set(CTSHELL_DEFINITIONS ${CTSHELL_DEFINITIONS} CACHE INTERNAL "ctshell definitions")
69+
70+
if(CTSHELL_ENABLE_FUZZING)
71+
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
72+
set(CTSHELL_FUZZ_TARGET ctshell_fuzz)
73+
set(CTSHELL_FUZZ_SRC "${CMAKE_CURRENT_SOURCE_DIR}/test/fuzz/ctshell_fuzz.c")
74+
add_executable(${CTSHELL_FUZZ_TARGET}
75+
${CTSHELL_FUZZ_SRC}
76+
${ctshell_srcs}
77+
)
78+
target_include_directories(${CTSHELL_FUZZ_TARGET} PRIVATE ${ctshell_incs})
79+
target_compile_definitions(${CTSHELL_FUZZ_TARGET} PRIVATE ${CTSHELL_DEFINITIONS})
80+
target_compile_options(${CTSHELL_FUZZ_TARGET} PRIVATE
81+
-fsanitize=fuzzer,address
82+
-fprofile-instr-generate
83+
-fcoverage-mapping
84+
-g
85+
-O1
86+
-mllvm
87+
-asan-globals=0
88+
)
89+
target_link_options(${CTSHELL_FUZZ_TARGET} PRIVATE
90+
-fsanitize=fuzzer,address
91+
-fprofile-instr-generate
92+
)
93+
else()
94+
message(FATAL_ERROR "Fuzzing requires Clang compiler. Run with: CC=clang cmake ..")
95+
endif()
96+
endif()
6797
endif()

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ctshell
22

3-
[![readthedocs](https://img.shields.io/readthedocs/ctshell)](https://ctshell.readthedocs.io/en/latest/) [![license](https://img.shields.io/github/license/MDLZCOOL/ctshell)](https://github.com/MDLZCOOL/ctshell/blob/main/LICENSE)
3+
[![readthedocs](https://img.shields.io/readthedocs/ctshell)](https://ctshell.readthedocs.io/en/latest/) [![build test](https://github.com/MDLZCOOL/ctshell/actions/workflows/build_test.yml/badge.svg)](https://github.com/MDLZCOOL/ctshell/actions?query=workflow%3A%22Build+Test%22) [![fuzz test](https://github.com/MDLZCOOL/ctshell/actions/workflows/fuzzing.yml/badge.svg)](https://github.com/MDLZCOOL/ctshell/actions?query=workflow%3AFuzzing) [![license](https://img.shields.io/github/license/MDLZCOOL/ctshell)](https://github.com/MDLZCOOL/ctshell/blob/main/LICENSE)
44

55
Ctshell is a low-overhead shell designed specifically for resource-constrained embedded systems.
66

test/fuzz/ctshell_fuzz.c

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2026, MDLZCOOL
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
#include <stdint.h>
7+
#include <stddef.h>
8+
#include <string.h>
9+
#include <stdlib.h>
10+
#include "ctshell.h"
11+
12+
static void dummy_shell_write(const char *str, uint16_t len, void *p) {
13+
CTSHELL_UNUSED_PARAM(str);
14+
CTSHELL_UNUSED_PARAM(len);
15+
CTSHELL_UNUSED_PARAM(p);
16+
}
17+
18+
static uint32_t dummy_get_tick(void) {
19+
static uint32_t tick = 0;
20+
return tick++;
21+
}
22+
23+
static int cmd_fuzz_complex(int argc, char *argv[]) {
24+
ctshell_arg_parser_t parser;
25+
ctshell_args_init(&parser, argc, argv);
26+
ctshell_expect_int(&parser, "-i", "--int");
27+
ctshell_expect_str(&parser, "-s", "--str");
28+
ctshell_expect_bool(&parser, "-v", "--verbose");
29+
ctshell_expect_verb(&parser, "start");
30+
ctshell_expect_verb(&parser, "stop");
31+
#ifdef CONFIG_CTSHELL_USE_DOUBLE
32+
ctshell_expect_double(&parser, "-d", "--double");
33+
#endif
34+
ctshell_args_parse(&parser);
35+
int i_val = ctshell_get_int(&parser, "-i");
36+
char *s_val = ctshell_get_str(&parser, "-s");
37+
int b_val = ctshell_get_bool(&parser, "-v");
38+
39+
CTSHELL_UNUSED_PARAM(i_val);
40+
CTSHELL_UNUSED_PARAM(s_val);
41+
CTSHELL_UNUSED_PARAM(b_val);
42+
43+
return 0;
44+
}
45+
CTSHELL_EXPORT_CMD(fuzz, cmd_fuzz_complex, "Fuzz complex args parsing", CTSHELL_ATTR_NONE);
46+
47+
/*
48+
* 注册根菜单: "net"
49+
* func=NULL, attr=MENU, 这是一个纯容器
50+
*/
51+
CTSHELL_EXPORT_CMD(net, NULL, "Network tools", CTSHELL_ATTR_MENU);
52+
53+
/*
54+
* 注册二级具体命令: "net ip"
55+
* parent="net", 挂载在 net 下
56+
* 这是一个叶子节点,输入 "net ip" 直接执行
57+
*/
58+
int cmd_net_ip(int argc, char *argv[]) {
59+
ctshell_printf("IP Address : 192.168.1.100\r\n");
60+
ctshell_printf("Subnet Mask: 255.255.255.0\r\n");
61+
return 0;
62+
}
63+
CTSHELL_EXPORT_SUBCMD(net, ip, cmd_net_ip, "Show IP address");
64+
65+
/*
66+
* 注册二级菜单容器: "net wifi"
67+
* parent="net"
68+
* func=NULL, 这是一个纯容器
69+
*/
70+
CTSHELL_EXPORT_SUBCMD(net, wifi, NULL, "WiFi management");
71+
72+
/*
73+
* 注册三级具体命令: "net wifi connect"
74+
* parent="net_wifi", 注意:父节点名是前两级名称的拼接 (net + _ + wifi)
75+
*/
76+
int cmd_wifi_connect(int argc, char *argv[]) {
77+
ctshell_arg_parser_t parser;
78+
ctshell_args_init(&parser, argc, argv);
79+
80+
// 定义参数: -s <ssid> 和 -p <password>
81+
ctshell_expect_str(&parser, "-s", "ssid");
82+
ctshell_expect_str(&parser, "-p", "password");
83+
84+
ctshell_args_parse(&parser);
85+
86+
if (ctshell_has(&parser, "ssid") && ctshell_has(&parser, "password")) {
87+
char *ssid = ctshell_get_str(&parser, "ssid");
88+
char *pwd = ctshell_get_str(&parser, "password");
89+
90+
ctshell_printf("Connecting to %s (Key: %s)...\r\n", ssid, pwd);
91+
} else {
92+
ctshell_printf("Usage: net wifi connect -s <ssid> -p <password>\r\n");
93+
}
94+
return 0;
95+
}
96+
CTSHELL_EXPORT_SUBCMD(net_wifi, connect, cmd_wifi_connect, "Connect to AP");
97+
98+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
99+
if (size > 4096) {
100+
return 0;
101+
}
102+
103+
ctshell_ctx_t ctx;
104+
memset(&ctx, 0, sizeof(ctx));
105+
ctshell_io_t io = {
106+
.write = dummy_shell_write,
107+
.get_tick = dummy_get_tick,
108+
};
109+
ctshell_init(&ctx, io, NULL);
110+
for (size_t i = 0; i < size; i++) {
111+
ctshell_input(&ctx, (char)data[i]);
112+
ctshell_poll(&ctx);
113+
}
114+
ctshell_poll(&ctx);
115+
return 0;
116+
}

0 commit comments

Comments
 (0)