Skip to content

Commit 9633cc3

Browse files
committed
gh: add ClusterFuzzLite fuzzing for lib/envmodules.c
The OSSF Scorecard "Fuzzing" check (security/code-scanning/7) only recognizes OSS-Fuzz registration or a .clusterfuzzlite/Dockerfile for C/C++ projects, not arbitrary libFuzzer harnesses on their own. Add that integration: .clusterfuzzlite/ holds the build script and one libFuzzer target per lib/envmodules.c entry point that parses externally-influenced input (date/time argument, file content, directory listing); the remaining entry points only query process/OS state and have no fuzzer-mutable input to target. cflite.yml runs a short pass on pull requests touching lib/ or .clusterfuzzlite/, and a longer bi-weekly/manual batch pass, mirroring the per-job permissions override scorecard.yml uses for the security-events write access needed to upload SARIF results. Document the new workflow in doc/source/devel/ci.rst and exclude .clusterfuzzlite/ from source archive exports in .gitattributes. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Xavier Delaruelle <xavier.delaruelle@cea.fr>
1 parent 6a07d9d commit 9633cc3

10 files changed

Lines changed: 415 additions & 4 deletions

File tree

‎.clusterfuzzlite/Dockerfile‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM gcr.io/oss-fuzz-base/base-builder:v1
2+
RUN apt-get update && apt-get install -y autoconf tcl8.6-dev
3+
COPY . $SRC/modules
4+
WORKDIR $SRC/modules
5+
COPY ./.clusterfuzzlite/build.sh $SRC/

‎.clusterfuzzlite/build.sh‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash -eu
2+
# shellcheck disable=SC2086,SC2016
3+
#
4+
# Flag variables below ($CFLAGS, $TCL_INCLUDE_SPEC, $TCL_LIB_SPEC,
5+
# $LIB_FUZZING_ENGINE, ...) are deliberately left unquoted so each word
6+
# splits into its own argument, per the standard OSS-Fuzz build.sh
7+
# convention; quoting would collapse a multi-flag string into one
8+
# argument and break the build.
9+
#
10+
# ClusterFuzzLite build script for lib/envmodules.c, the C extension
11+
# backing the "libtclenvmodules" Tcl package. Fuzz targets call the
12+
# extension's ObjCmd entry points directly against a real Tcl interpreter
13+
# (Tcl_CreateInterp), bypassing Envmodules_Init, so this links against
14+
# libtcl itself rather than the Tcl stub library the shipped module uses.
15+
16+
MODROOT="$SRC/modules"
17+
FUZZDIR="$MODROOT/.clusterfuzzlite"
18+
19+
# Generate lib/config.h (PACKAGE_NAME, GETGROUPS_T, ...) through the
20+
# project's own TEA-based configure script. --disable-shared
21+
# --disable-stubs turns off USE_TCL_STUBS: the shipped module is a
22+
# stub-linked loadable extension whose Tcl_* calls only resolve once
23+
# Envmodules_Init() runs Tcl_InitStubs(), but these fuzz targets call the
24+
# ObjCmd entry points directly and link against libtcl itself, which
25+
# doesn't export tclStubsPtr/Tcl_InitStubs.
26+
cd "$MODROOT/lib"
27+
TCLCONFDIR=$(dirname "$(find /usr -name tclConfig.sh | head -n1)")
28+
./configure --with-tcl="$TCLCONFDIR" --disable-shared --disable-stubs
29+
30+
# shellcheck disable=SC1091
31+
. "$TCLCONFDIR/tclConfig.sh"
32+
33+
$CC $CFLAGS $TCL_INCLUDE_SPEC -I"$MODROOT/lib" \
34+
-c "$MODROOT/lib/envmodules.c" -o "$WORK/envmodules.o"
35+
36+
for fuzzer in fuzz_parsedatetimearg fuzz_readfile fuzz_getfilesindirectory; do
37+
$CC $CFLAGS $TCL_INCLUDE_SPEC -I"$MODROOT/lib" \
38+
-c "$FUZZDIR/$fuzzer.c" -o "$WORK/$fuzzer.o"
39+
$CXX $CXXFLAGS -Wl,-rpath,'$ORIGIN/lib' \
40+
"$WORK/$fuzzer.o" "$WORK/envmodules.o" \
41+
$TCL_LIB_SPEC $LIB_FUZZING_ENGINE -o "$OUT/$fuzzer"
42+
done
43+
44+
# Bundle libtcl next to the fuzz targets: $OUT ships without the
45+
# container's system packages.
46+
mkdir -p "$OUT/lib"
47+
TCL_LIBDIR=$(echo "$TCL_LIB_SPEC" | grep -oE -- '-L[^ ]+' | head -n1 | cut -c3-)
48+
cp -L "$TCL_LIBDIR"/libtcl8*.so* "$OUT/lib/"
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* ClusterFuzzLite target for Envmodules_GetFilesInDirectoryObjCmd, which
3+
* lists a directory while special-casing .modulerc/.version and hidden
4+
* entries. Module directories can live on shared, multi-tenant
5+
* filesystems, so this exercises the entry-name handling with
6+
* fuzzer-controlled file names rather than only fuzzer-controlled paths.
7+
*/
8+
9+
#define _GNU_SOURCE
10+
#include <stdint.h>
11+
#include <stddef.h>
12+
#include <stdlib.h>
13+
#include <string.h>
14+
#include <limits.h>
15+
#include <dirent.h>
16+
#include <unistd.h>
17+
#include <fcntl.h>
18+
#include <stdio.h>
19+
#include "envmodules.h"
20+
21+
#define MAX_ENTRIES 32
22+
#define MAX_NAME_LEN 200
23+
24+
static void
25+
cleanupDir(
26+
const char *dir)
27+
{
28+
DIR *d;
29+
struct dirent *de;
30+
char fpath[PATH_MAX];
31+
32+
d = opendir(dir);
33+
if (d == NULL) {
34+
return;
35+
}
36+
while ((de = readdir(d)) != NULL) {
37+
if (strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0) {
38+
snprintf(fpath, sizeof(fpath), "%s/%s", dir, de->d_name);
39+
unlink(fpath);
40+
}
41+
}
42+
closedir(d);
43+
}
44+
45+
int
46+
LLVMFuzzerTestOneInput(
47+
const uint8_t *data,
48+
size_t size)
49+
{
50+
char dirtemplate[] = "/tmp/cflite_gfid.XXXXXX";
51+
char *dir;
52+
size_t start, i;
53+
int created = 0;
54+
Tcl_Interp *interp;
55+
Tcl_Obj *objv[3];
56+
57+
if (size < 1) {
58+
return 0;
59+
}
60+
61+
dir = mkdtemp(dirtemplate);
62+
if (dir == NULL) {
63+
return 0;
64+
}
65+
66+
/* Split the input (past the leading flag byte) on newlines into
67+
* candidate file names. */
68+
start = 1;
69+
for (i = 1; i <= size && created < MAX_ENTRIES; i++) {
70+
if (i == size || data[i] == '\n') {
71+
size_t len = i - start;
72+
if (len > 0 && len < MAX_NAME_LEN) {
73+
char name[MAX_NAME_LEN + 1];
74+
char fpath[PATH_MAX];
75+
int fd;
76+
77+
memcpy(name, data + start, len);
78+
name[len] = '\0';
79+
if (strchr(name, '/') == NULL && strcmp(name, ".") != 0 &&
80+
strcmp(name, "..") != 0) {
81+
snprintf(fpath, sizeof(fpath), "%s/%s", dir, name);
82+
fd = open(fpath, O_CREAT | O_WRONLY, 0600);
83+
if (fd != -1) {
84+
close(fd);
85+
created++;
86+
}
87+
}
88+
}
89+
start = i + 1;
90+
}
91+
}
92+
93+
interp = Tcl_CreateInterp();
94+
95+
objv[0] = Tcl_NewStringObj("getFilesInDirectory", -1);
96+
objv[1] = Tcl_NewStringObj(dir, -1);
97+
objv[2] = Tcl_NewBooleanObj(data[0] & 1);
98+
Tcl_IncrRefCount(objv[0]);
99+
Tcl_IncrRefCount(objv[1]);
100+
Tcl_IncrRefCount(objv[2]);
101+
102+
Envmodules_GetFilesInDirectoryObjCmd(NULL, interp, 3, objv);
103+
104+
Tcl_DecrRefCount(objv[0]);
105+
Tcl_DecrRefCount(objv[1]);
106+
Tcl_DecrRefCount(objv[2]);
107+
Tcl_DeleteInterp(interp);
108+
109+
cleanupDir(dir);
110+
rmdir(dir);
111+
112+
return 0;
113+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* ClusterFuzzLite target for Envmodules_ParseDateTimeArgObjCmd, which
3+
* parses a "YYYY-MM-DD[THH:MM]" argument value into Epoch time.
4+
*/
5+
6+
#include <stdint.h>
7+
#include <stddef.h>
8+
#include "envmodules.h"
9+
10+
int
11+
LLVMFuzzerTestOneInput(
12+
const uint8_t *data,
13+
size_t size)
14+
{
15+
Tcl_Interp *interp;
16+
Tcl_Obj *objv[3];
17+
18+
interp = Tcl_CreateInterp();
19+
20+
objv[0] = Tcl_NewStringObj("parseDateTimeArg", -1);
21+
objv[1] = Tcl_NewStringObj("opt", -1);
22+
objv[2] = Tcl_NewStringObj((const char *) data, (int) size);
23+
Tcl_IncrRefCount(objv[0]);
24+
Tcl_IncrRefCount(objv[1]);
25+
Tcl_IncrRefCount(objv[2]);
26+
27+
Envmodules_ParseDateTimeArgObjCmd(NULL, interp, 3, objv);
28+
29+
Tcl_DecrRefCount(objv[0]);
30+
Tcl_DecrRefCount(objv[1]);
31+
Tcl_DecrRefCount(objv[2]);
32+
Tcl_DeleteInterp(interp);
33+
34+
return 0;
35+
}

‎.clusterfuzzlite/fuzz_readfile.c‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* ClusterFuzzLite target for Envmodules_ReadFileObjCmd, which opens,
3+
* reads and closes a file while looking for the "#%Module" magic cookie
4+
* on its first line.
5+
*
6+
* The fuzzer input is written to a memfd instead of a real filesystem
7+
* path so each run stays off disk; the readFile command still receives
8+
* an ordinary path, via /proc/self/fd/<n>.
9+
*/
10+
11+
#define _GNU_SOURCE
12+
#include <stdint.h>
13+
#include <stddef.h>
14+
#include <stdio.h>
15+
#include <sys/mman.h>
16+
#include <unistd.h>
17+
#include "envmodules.h"
18+
19+
int
20+
LLVMFuzzerTestOneInput(
21+
const uint8_t *data,
22+
size_t size)
23+
{
24+
int fd;
25+
char path[64];
26+
Tcl_Interp *interp;
27+
Tcl_Obj *objv[4];
28+
29+
if (size < 1) {
30+
return 0;
31+
}
32+
33+
/* First input byte selects the firstline/must_have_cookie flags; the
34+
* remaining bytes become the file content read back by readFile. */
35+
fd = memfd_create("cflite_readfile", 0);
36+
if (fd == -1) {
37+
return 0;
38+
}
39+
if (write(fd, data + 1, size - 1) != (ssize_t) (size - 1)) {
40+
close(fd);
41+
return 0;
42+
}
43+
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
44+
45+
interp = Tcl_CreateInterp();
46+
47+
objv[0] = Tcl_NewStringObj("readFile", -1);
48+
objv[1] = Tcl_NewStringObj(path, -1);
49+
objv[2] = Tcl_NewBooleanObj(data[0] & 1);
50+
objv[3] = Tcl_NewBooleanObj((data[0] >> 1) & 1);
51+
Tcl_IncrRefCount(objv[0]);
52+
Tcl_IncrRefCount(objv[1]);
53+
Tcl_IncrRefCount(objv[2]);
54+
Tcl_IncrRefCount(objv[3]);
55+
56+
Envmodules_ReadFileObjCmd(NULL, interp, 4, objv);
57+
58+
Tcl_DecrRefCount(objv[0]);
59+
Tcl_DecrRefCount(objv[1]);
60+
Tcl_DecrRefCount(objv[2]);
61+
Tcl_DecrRefCount(objv[3]);
62+
Tcl_DeleteInterp(interp);
63+
close(fd);
64+
65+
return 0;
66+
}

‎.clusterfuzzlite/project.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
language: c

‎.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
version.inc.in export-subst
33
# no export of git-specific stuff
44
.github export-ignore
5+
.clusterfuzzlite export-ignore
56
.gitignore export-ignore
67
.gitattributes export-ignore
78
init/.gitignore export-ignore

‎.github/workflows/cflite.yml‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: ClusterFuzzLite fuzzing
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'lib/**'
7+
- '.clusterfuzzlite/**'
8+
- '.github/workflows/cflite.yml'
9+
schedule:
10+
# bi-weekly, 03:00 UTC on the 1st and 15th of each month; cron has no
11+
# native "every 2 weeks" field, so day-of-month is the usual
12+
# approximation for a bi-weekly cadence
13+
- cron: '0 3 1,15 * *'
14+
workflow_dispatch:
15+
16+
permissions: read-all
17+
18+
jobs:
19+
PR:
20+
if: github.event_name == 'pull_request'
21+
runs-on: ubuntu-latest
22+
permissions:
23+
# output-sarif: true below has run_fuzzers upload results to code
24+
# scanning, which needs write access to that API
25+
security-events: write
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }}
28+
cancel-in-progress: true
29+
strategy:
30+
fail-fast: false
31+
matrix:
32+
sanitizer:
33+
- address
34+
- undefined
35+
steps:
36+
- name: Build Fuzzers (${{ matrix.sanitizer }})
37+
id: build
38+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
39+
with:
40+
language: c
41+
github-token: ${{ secrets.GITHUB_TOKEN }}
42+
sanitizer: ${{ matrix.sanitizer }}
43+
- name: Run Fuzzers (${{ matrix.sanitizer }})
44+
id: run
45+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
46+
with:
47+
github-token: ${{ secrets.GITHUB_TOKEN }}
48+
fuzz-seconds: 300
49+
mode: 'code-change'
50+
sanitizer: ${{ matrix.sanitizer }}
51+
output-sarif: true
52+
53+
BatchFuzzing:
54+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
55+
runs-on: ubuntu-latest
56+
permissions:
57+
security-events: write
58+
strategy:
59+
fail-fast: false
60+
matrix:
61+
sanitizer:
62+
- address
63+
- undefined
64+
steps:
65+
- name: Build Fuzzers (${{ matrix.sanitizer }})
66+
id: build
67+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
68+
with:
69+
language: c
70+
sanitizer: ${{ matrix.sanitizer }}
71+
- name: Run Fuzzers (${{ matrix.sanitizer }})
72+
id: run
73+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
74+
with:
75+
github-token: ${{ secrets.GITHUB_TOKEN }}
76+
fuzz-seconds: 3600
77+
mode: 'batch'
78+
sanitizer: ${{ matrix.sanitizer }}
79+
output-sarif: true

‎.hunspell.en.dic‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,3 +1496,22 @@ tcl90
14961496
tcl91
14971497
testutil
14981498
xvfb
1499+
ClusterFuzzLite
1500+
Dockerfile
1501+
OSS
1502+
libFuzzer
1503+
cflite
1504+
clusterfuzzlite
1505+
fuzzer
1506+
gitattributes
1507+
AddressSanitizer
1508+
BatchFuzzing
1509+
InitStateClockSecondsObjCmd
1510+
InitStateUsernameObjCmd
1511+
UndefinedBehaviorSanitizer
1512+
getfilesindirectory
1513+
libtcl
1514+
parsedatetimearg
1515+
readfile
1516+
request's
1517+
uid

0 commit comments

Comments
 (0)