From aa2b60be7df0fa13c79a0bab132c6a1664587c6c Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 21 Mar 2026 22:18:31 +0100 Subject: [PATCH] Fix 29 CodeQL alerts: command injection, snprintf overflow, TOCTOU races - Command injection (CRITICAL): validate shell args before system() in update command's unzip and version-check calls - TOCTOU cli.c: use open(O_CREAT, 0755) + fdopen() to set permissions atomically instead of fopen() + chmod() after close - TOCTOU pass_envscan.c: open file first, then fstat() on fd to check size, eliminating stat-then-open race window - Overflowing snprintf (11 locations): clamp offset after each append to prevent unsigned underflow on truncation in cypher.c, store.c, http_server.c, test_c_lsp.c - Add CBM_SNPRINTF_APPEND macro in str_util.h for future safe appends - CodeQL: remove pull_request trigger (only scan push to main) - CodeQL gate: increase timeout from 30 to 45 minutes - Add fuzz testing script (random JSON-RPC + Cypher mutations) - 12 Scorecard governance alerts dismissed (not code vulnerabilities) --- .github/workflows/codeql.yml | 2 - .github/workflows/dry-run.yml | 14 ++- .github/workflows/release.yml | 14 ++- SECURITY.md | 1 + scripts/security-fuzz-random.sh | 168 ++++++++++++++++++++++++++++++++ src/cli/cli.c | 36 +++++-- src/cypher/cypher.c | 19 +++- src/foundation/str_util.h | 18 ++++ src/pipeline/pass_envscan.c | 14 +-- src/store/store.c | 30 ++++-- src/ui/http_server.c | 6 ++ tests/test_c_lsp.c | 8 +- 12 files changed, 291 insertions(+), 39 deletions(-) create mode 100755 scripts/security-fuzz-random.sh diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 56cad7fef..30c585265 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,8 +3,6 @@ name: CodeQL SAST on: push: branches: [main] - pull_request: - branches: [main] permissions: security-events: write diff --git a/.github/workflows/dry-run.yml b/.github/workflows/dry-run.yml index 5082d33c9..811a39082 100644 --- a/.github/workflows/dry-run.yml +++ b/.github/workflows/dry-run.yml @@ -80,7 +80,7 @@ jobs: if: ${{ !inputs.skip_lint }} runs-on: ubuntu-latest steps: - - name: Wait for CodeQL on current commit (max 30 min) + - name: Wait for CodeQL on current commit (max 45 min) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -88,12 +88,12 @@ jobs: echo "Current commit: $CURRENT_SHA" echo "Waiting for CodeQL to complete on this commit..." - for attempt in $(seq 1 60); do + for attempt in $(seq 1 90); do LATEST=$(gh api repos/${{ github.repository }}/actions/workflows/codeql.yml/runs?per_page=5 \ --jq '.workflow_runs[] | select(.head_sha == "'"$CURRENT_SHA"'") | "\(.conclusion) \(.status)"' 2>/dev/null | head -1 || echo "") if [ -z "$LATEST" ]; then - echo " Attempt $attempt/60: No CodeQL run found for $CURRENT_SHA yet..." + echo " Attempt $attempt/90: No CodeQL run found for $CURRENT_SHA yet..." sleep 30 continue fi @@ -109,11 +109,11 @@ jobs: exit 1 fi - echo " Attempt $attempt/60: CodeQL status=$STATUS (waiting 30s)..." + echo " Attempt $attempt/90: CodeQL status=$STATUS (waiting 30s)..." sleep 30 done - echo "BLOCKED: CodeQL did not complete within 30 minutes" + echo "BLOCKED: CodeQL did not complete within 45 minutes" exit 1 - name: Check for open code scanning alerts @@ -358,6 +358,10 @@ jobs: if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64' run: scripts/security-fuzz.sh ./codebase-memory-mcp + - name: Fuzz testing (60s random input) + if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64' + run: scripts/security-fuzz-random.sh ./codebase-memory-mcp 60 + - name: ClamAV scan (Linux) if: matrix.variant == 'standard' && startsWith(matrix.os, 'ubuntu') run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24b8a16ab..da4b18a1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: codeql-gate: runs-on: ubuntu-latest steps: - - name: Wait for CodeQL on current commit (max 30 min) + - name: Wait for CodeQL on current commit (max 45 min) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -90,12 +90,12 @@ jobs: echo "Current commit: $CURRENT_SHA" echo "Waiting for CodeQL to complete on this commit..." - for attempt in $(seq 1 60); do + for attempt in $(seq 1 90); do LATEST=$(gh api repos/${{ github.repository }}/actions/workflows/codeql.yml/runs?per_page=5 \ --jq '.workflow_runs[] | select(.head_sha == "'"$CURRENT_SHA"'") | "\(.conclusion) \(.status)"' 2>/dev/null | head -1 || echo "") if [ -z "$LATEST" ]; then - echo " Attempt $attempt/60: No CodeQL run found for $CURRENT_SHA yet..." + echo " Attempt $attempt/90: No CodeQL run found for $CURRENT_SHA yet..." sleep 30 continue fi @@ -111,11 +111,11 @@ jobs: exit 1 fi - echo " Attempt $attempt/60: CodeQL status=$STATUS (waiting 30s)..." + echo " Attempt $attempt/90: CodeQL status=$STATUS (waiting 30s)..." sleep 30 done - echo "BLOCKED: CodeQL did not complete within 30 minutes" + echo "BLOCKED: CodeQL did not complete within 45 minutes" exit 1 - name: Check for open code scanning alerts @@ -354,6 +354,10 @@ jobs: if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64' run: scripts/security-fuzz.sh ./codebase-memory-mcp + - name: Fuzz testing (60s random input) + if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64' + run: scripts/security-fuzz-random.sh ./codebase-memory-mcp 60 + # Native platform antivirus scan - name: ClamAV scan (Linux) if: matrix.variant == 'standard' && startsWith(matrix.os, 'ubuntu') diff --git a/SECURITY.md b/SECURITY.md index 5a89506db..578d881ac 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,6 +29,7 @@ This project implements multiple layers of security verification. Every release - **Time-bomb pattern detection** — scans for `time()`/`sleep()` near dangerous calls (could indicate delayed activation) - **MCP tool handler file read audit** — tracks file read count in `mcp.c` against an expected maximum (detects added file reads that could exfiltrate data through tool responses) - **CodeQL SAST** — static application security testing on every push (taint analysis, CWE detection, data flow tracking). Any open alert blocks the release. +- **Fuzz testing** — random/mutated inputs to MCP server and Cypher parser (60 seconds per build). Catches crashes, segfaults, and memory errors that structured tests miss. - **Native antivirus scanning** on every platform (any detection fails the build): - **Windows**: Windows Defender with ML heuristics — the same engine end users run - **Linux**: ClamAV with daily signature updates diff --git a/scripts/security-fuzz-random.sh b/scripts/security-fuzz-random.sh new file mode 100755 index 000000000..5015a9df2 --- /dev/null +++ b/scripts/security-fuzz-random.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fuzz testing: feeds random/mutated inputs to the MCP server and CLI +# to find crashes, hangs, and memory errors. Runs for a limited time. +# +# Usage: scripts/security-fuzz-random.sh [duration_seconds] + +BINARY="${1:?usage: security-fuzz-random.sh [duration_seconds]}" +DURATION="${2:-60}" + +if [[ ! -f "$BINARY" ]]; then + echo "FAIL: binary not found: $BINARY" + exit 1 +fi + +echo "=== Fuzz Testing ($DURATION seconds) ===" + +FUZZ_TMPDIR=$(mktemp -d) +trap 'rm -rf "$FUZZ_TMPDIR"' EXIT + +CRASHES=0 +ITERATIONS=0 +END_TIME=$((SECONDS + DURATION)) + +# Portable timeout +run_with_timeout() { + local secs="$1"; shift + if command -v timeout &>/dev/null; then + timeout "$secs" "$@" || true + else + perl -e "alarm($secs); exec @ARGV" -- "$@" || true + fi +} + +# ── Phase 1: Random JSON-RPC mutations ─────────────────────── +echo "" +echo "--- Phase 1: Random JSON-RPC mutations ---" + +while [ $SECONDS -lt $END_TIME ]; do + ITERATIONS=$((ITERATIONS + 1)) + + # Generate a random mutated JSON-RPC payload + PAYLOAD=$(python3 -c " +import random, string, json + +# Base valid request +base = { + 'jsonrpc': '2.0', + 'id': random.randint(-999999, 999999), + 'method': random.choice([ + 'initialize', 'tools/call', 'tools/list', + random.choice(string.ascii_letters) * random.randint(1, 100), + '', + ]), +} + +# Random mutations +mutation = random.randint(0, 10) +if mutation == 0: + # Valid tool call with random args + base['params'] = {'name': 'search_graph', 'arguments': { + 'name_pattern': ''.join(random.choices(string.printable, k=random.randint(0, 500))) + }} +elif mutation == 1: + # Huge nested object + base['params'] = {'a': {'b': {'c': {'d': {'e': 'deep'}}}}} +elif mutation == 2: + # Random bytes as method + base['method'] = ''.join(random.choices(string.printable, k=random.randint(1, 1000))) +elif mutation == 3: + # Null fields + base['method'] = None + base['id'] = None +elif mutation == 4: + # Array instead of object + base = [1, 2, 3] +elif mutation == 5: + # Empty + base = {} +elif mutation == 6: + # Random Cypher query + base['params'] = {'name': 'query_graph', 'arguments': { + 'query': ''.join(random.choices(string.printable, k=random.randint(1, 200))) + }} +elif mutation == 7: + # Very long string values + base['params'] = {'name': 'search_graph', 'arguments': { + 'name_pattern': 'A' * random.randint(10000, 100000) + }} +elif mutation == 8: + # Unicode/binary-like content + base['params'] = {'name': 'search_code', 'arguments': { + 'pattern': ''.join(chr(random.randint(0, 0xFFFF)) for _ in range(100)) + }} +elif mutation == 9: + # Random tool name + base['params'] = {'name': ''.join(random.choices(string.ascii_letters, k=50)), 'arguments': {}} +else: + # Completely random JSON + base = ''.join(random.choices(string.printable, k=random.randint(1, 500))) + +try: + print(json.dumps(base)) +except: + print(json.dumps({'garbage': True})) +" 2>/dev/null || echo '{}') + + # Build session: init + mutated payload + INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"fuzz","version":"1.0"}}}' + printf '%s\n%s\n' "$INIT" "$PAYLOAD" > "$FUZZ_TMPDIR/input.jsonl" + + # Run and check for crashes (not exit code — we expect errors, just not crashes) + run_with_timeout 5 "$BINARY" < "$FUZZ_TMPDIR/input.jsonl" > /dev/null 2>&1 + EC=$? + + # 139 = SIGSEGV, 134 = SIGABRT, 136 = SIGFPE — real crashes + if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then + echo "CRASH: exit code $EC on iteration $ITERATIONS" + echo "Payload: $PAYLOAD" + CRASHES=$((CRASHES + 1)) + fi +done + +echo "Phase 1: $ITERATIONS iterations, $CRASHES crashes" + +# ── Phase 2: Random Cypher queries via CLI ─────────────────── +echo "" +echo "--- Phase 2: Random Cypher queries via CLI ---" + +CLI_ITERATIONS=0 +CLI_END=$((SECONDS + 10)) + +while [ $SECONDS -lt $CLI_END ]; do + CLI_ITERATIONS=$((CLI_ITERATIONS + 1)) + + QUERY=$(python3 -c " +import random, string +parts = ['MATCH', 'WHERE', 'RETURN', '(', ')', '-[', ']->', '<-[', ']-', + 'n', 'r', '.name', '.label', '=', '\"', \"'\", ';', '--', 'DROP', + 'DELETE', 'ATTACH', 'DETACH', '*', 'COUNT', 'LIMIT', 'ORDER BY'] +q = ' '.join(random.choices(parts, k=random.randint(1, 20))) +# Sometimes add random garbage +if random.random() > 0.5: + q += ''.join(random.choices(string.printable, k=random.randint(1, 100))) +print(q) +" 2>/dev/null || echo "MATCH (n) RETURN n") + + run_with_timeout 3 "$BINARY" cli query_graph "{\"query\":\"$QUERY\"}" > /dev/null 2>&1 + EC=$? + + if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then + echo "CRASH: exit code $EC on Cypher iteration $CLI_ITERATIONS" + echo "Query: $QUERY" + CRASHES=$((CRASHES + 1)) + fi +done + +echo "Phase 2: $CLI_ITERATIONS iterations, $CRASHES total crashes" + +# ── Summary ────────────────────────────────────────────────── +echo "" +if [ $CRASHES -gt 0 ]; then + echo "=== FUZZ TESTING FAILED: $CRASHES crash(es) found ===" + exit 1 +fi + +echo "=== Fuzz testing passed: $((ITERATIONS + CLI_ITERATIONS)) iterations, 0 crashes ===" diff --git a/src/cli/cli.c b/src/cli/cli.c index e1fc4bec9..a2353671e 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -6,6 +6,7 @@ */ #include "cli/cli.h" #include "foundation/compat.h" +#include "foundation/str_util.h" // the correct standard headers are included below but clang-tidy doesn't map them. #include @@ -15,6 +16,7 @@ #define CBM_VERSION "dev" #endif #include // EEXIST +#include // open, O_WRONLY, O_CREAT, O_TRUNC #include // uintptr_t #include #include @@ -2784,22 +2786,36 @@ int cbm_cmd_update(int argc, char **argv) { return 1; } + /* Open with final permissions atomically (no TOCTOU between write and chmod) */ +#ifndef _WIN32 + int fd = open(bin_dest, O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (fd < 0) { + fprintf(stderr, "error: cannot write to %s\n", bin_dest); + free(bin_data); + return 1; + } + FILE *out = fdopen(fd, "wb"); +#else FILE *out = fopen(bin_dest, "wb"); +#endif if (!out) { fprintf(stderr, "error: cannot write to %s\n", bin_dest); free(bin_data); +#ifndef _WIN32 + close(fd); +#endif return 1; } fwrite(bin_data, 1, (size_t)bin_len, out); fclose(out); free(bin_data); - - /* Make executable */ -#ifndef _WIN32 - chmod(bin_dest, 0755); -#endif } else { - /* Windows: unzip */ + /* Windows: unzip — validate paths before shell interpolation */ + if (!cbm_validate_shell_arg(bin_dir) || !cbm_validate_shell_arg(tmp_archive)) { + fprintf(stderr, "error: path contains unsafe characters\n"); + cbm_unlink(tmp_archive); + return 1; + } snprintf(cmd, sizeof(cmd), "unzip -o -d '%s' '%s' 2>/dev/null", bin_dir, tmp_archive); // NOLINTNEXTLINE(cert-env33-c) — intentional CLI subprocess for extraction rc = system(cmd); @@ -2825,9 +2841,11 @@ int cbm_cmd_update(int argc, char **argv) { /* Step 7: Verify new version */ printf("\nUpdate complete. Verifying:\n"); - snprintf(cmd, sizeof(cmd), "'%s' --version", bin_dest); - // NOLINTNEXTLINE(cert-env33-c,clang-analyzer-optin.taint.GenericTaint) - (void)system(cmd); + if (cbm_validate_shell_arg(bin_dest)) { + snprintf(cmd, sizeof(cmd), "'%s' --version", bin_dest); + // NOLINTNEXTLINE(cert-env33-c,clang-analyzer-optin.taint.GenericTaint) + (void)system(cmd); + } printf("\nAll project indexes were cleared. They will be rebuilt\n"); printf("automatically when you next use the MCP server.\n"); diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index b7e1c159a..64985cbcb 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -2561,7 +2561,10 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec } const char *v = binding_get_virtual(&bindings[bi], wc->items[ci].variable, wc->items[ci].property); - kl += snprintf(key + kl, sizeof(key) - kl, "%s|", v); + kl += snprintf(key + kl, sizeof(key) - (size_t)kl, "%s|", v); + if (kl >= (int)sizeof(key)) { + kl = (int)sizeof(key) - 1; + } } int found = -1; for (int a = 0; a < agg_cnt; a++) { @@ -2918,7 +2921,10 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec } char func_buf[512]; vals[ci] = project_item(&bindings[bi], item, func_buf, sizeof(func_buf)); - klen += snprintf(key + klen, sizeof(key) - klen, "%s|", vals[ci]); + klen += snprintf(key + klen, sizeof(key) - (size_t)klen, "%s|", vals[ci]); + if (klen >= (int)sizeof(key)) { + klen = (int)sizeof(key) - 1; + } } int found = -1; @@ -3005,10 +3011,15 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec if (ci2 > 0) { cbuf[bl++] = ','; } - bl += snprintf(cbuf + bl, sizeof(cbuf) - bl, "\"%s\"", + bl += snprintf(cbuf + bl, sizeof(cbuf) - (size_t)bl, "\"%s\"", aggs[a].collect_lists[ci][ci2]); + if (bl >= (int)sizeof(cbuf)) { + bl = (int)sizeof(cbuf) - 1; + } + } + if (bl < (int)sizeof(cbuf) - 1) { + cbuf[bl++] = ']'; } - cbuf[bl++] = ']'; cbuf[bl] = '\0'; snprintf(bufs[ci], sizeof(bufs[ci]), "%s", cbuf); } else { diff --git a/src/foundation/str_util.h b/src/foundation/str_util.h index 5ecf479a6..6eccc1439 100644 --- a/src/foundation/str_util.h +++ b/src/foundation/str_util.h @@ -53,4 +53,22 @@ char **cbm_str_split(CBMArena *a, const char *s, char delim, int *out_count); * Returns true if safe, false if the string contains shell metacharacters. */ bool cbm_validate_shell_arg(const char *s); +/* Safe snprintf append: clamps offset to prevent buffer overflow on truncation. + * When snprintf truncates, it returns what it WOULD have written, which can make + * offset > bufsize. Next call: bufsize - offset wraps unsigned → huge → overflow. + * This macro guards against that by checking bounds before writing and clamping after. + * + * Usage: CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, "fmt %s", arg); + * Requires: included by caller. */ +#define CBM_SNPRINTF_APPEND(buf, sz, off, ...) \ + do { \ + if ((off) >= 0 && (off) < (int)(sz)) { \ + int _cbm_r = snprintf((buf) + (off), (sz) - (size_t)(off), __VA_ARGS__); \ + if (_cbm_r > 0) \ + (off) += _cbm_r; \ + if ((off) >= (int)(sz)) \ + (off) = (int)(sz) - 1; \ + } \ + } while (0) + #endif /* CBM_STR_UTIL_H */ diff --git a/src/pipeline/pass_envscan.c b/src/pipeline/pass_envscan.c index 6f0f3cd9c..af8e64281 100644 --- a/src/pipeline/pass_envscan.c +++ b/src/pipeline/pass_envscan.c @@ -383,11 +383,6 @@ int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int continue; } - /* Skip large files (>1MB) */ - if (st.st_size > (long)1024 * 1024) { - continue; - } - /* Determine file type */ file_type_t ft = detect_file_type(ent->name); if (ft == FT_UNKNOWN) { @@ -400,12 +395,19 @@ int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int rel++; } - /* Read and scan file line by line */ + /* Open first, then fstat on fd to avoid TOCTOU race */ FILE *f = fopen(full_path, "r"); if (!f) { continue; } + /* Recheck size on the open fd (not the path) */ + struct stat fst; + if (fstat(fileno(f), &fst) != 0 || fst.st_size > (long)1024 * 1024) { + fclose(f); + continue; + } + char line[2048]; while (fgets(line, sizeof(line), f) && count < max_out) { /* Strip trailing newline */ diff --git a/src/store/store.c b/src/store/store.c index 0c3c6d33c..4b00d4e73 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -1895,10 +1895,16 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear int elen = (int)strlen(excl_clause); for (int i = 0; params->exclude_labels[i]; i++) { if (i > 0) { - elen += snprintf(excl_clause + elen, sizeof(excl_clause) - elen, ","); + elen += snprintf(excl_clause + elen, sizeof(excl_clause) - (size_t)elen, ","); + if (elen >= (int)sizeof(excl_clause)) { + elen = (int)sizeof(excl_clause) - 1; + } } - elen += snprintf(excl_clause + elen, sizeof(excl_clause) - elen, "'%s'", + elen += snprintf(excl_clause + elen, sizeof(excl_clause) - (size_t)elen, "'%s'", params->exclude_labels[i]); + if (elen >= (int)sizeof(excl_clause)) { + elen = (int)sizeof(excl_clause) - 1; + } } snprintf(excl_clause + elen, sizeof(excl_clause) - (size_t)elen, ")"); ADD_WHERE(excl_clause); @@ -2040,10 +2046,16 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const int tlen = 0; for (int i = 0; i < edge_type_count; i++) { if (i > 0) { - tlen += snprintf(types_clause + tlen, sizeof(types_clause) - tlen, ","); + tlen += snprintf(types_clause + tlen, sizeof(types_clause) - (size_t)tlen, ","); + if (tlen >= (int)sizeof(types_clause)) { + tlen = (int)sizeof(types_clause) - 1; + } + } + tlen += snprintf(types_clause + tlen, sizeof(types_clause) - (size_t)tlen, "'%s'", + edge_types[i]); + if (tlen >= (int)sizeof(types_clause)) { + tlen = (int)sizeof(types_clause) - 1; } - tlen += - snprintf(types_clause + tlen, sizeof(types_clause) - tlen, "'%s'", edge_types[i]); } } @@ -2111,9 +2123,15 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const /* Build ID set: root + all visited */ char id_set[4096]; int ilen = snprintf(id_set, sizeof(id_set), "%lld", (long long)start_id); + if (ilen >= (int)sizeof(id_set)) { + ilen = (int)sizeof(id_set) - 1; + } for (int i = 0; i < n; i++) { - ilen += snprintf(id_set + ilen, sizeof(id_set) - ilen, ",%lld", + ilen += snprintf(id_set + ilen, sizeof(id_set) - (size_t)ilen, ",%lld", (long long)out->visited[i].node.id); + if (ilen >= (int)sizeof(id_set)) { + ilen = (int)sizeof(id_set) - 1; + } } char edge_sql[8192]; diff --git a/src/ui/http_server.c b/src/ui/http_server.c index eac56c924..a2a30f374 100644 --- a/src/ui/http_server.c +++ b/src/ui/http_server.c @@ -274,6 +274,9 @@ static void handle_processes(struct mg_connection *c) { "\"elapsed\":\"%s\",\"command\":\"%s\",\"is_self\":%s}", pid, (double)cpu, (double)rss / 1024.0, elapsed, comm, pid == (int)getpid() ? "true" : "false"); + if (pos >= (int)sizeof(buf)) { + pos = (int)sizeof(buf) - 1; + } proc_count++; } } @@ -415,6 +418,9 @@ static void handle_browse(struct mg_connection *c, struct mg_http_message *hm) { if (count > 0) buf[pos++] = ','; pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "\"%s\"", ent->d_name); + if (pos >= (int)sizeof(buf)) { + pos = (int)sizeof(buf) - 1; + } count++; if (count >= 200) diff --git a/tests/test_c_lsp.c b/tests/test_c_lsp.c index 256c2463e..a146b23e9 100644 --- a/tests/test_c_lsp.c +++ b/tests/test_c_lsp.c @@ -7050,8 +7050,12 @@ TEST(clsp_nocrash_extremely_large_function) { int off = 0; off += snprintf(src + off, sizeof(src) - off, "struct W { void m() {} };\n"); off += snprintf(src + off, sizeof(src) - off, "void test() {\n"); - for (int i = 0; i < 50; i++) - off += snprintf(src + off, sizeof(src) - off, " W w%d;\n", i); + for (int i = 0; i < 50; i++) { + off += snprintf(src + off, sizeof(src) - (size_t)off, " W w%d;\n", i); + if (off >= (int)sizeof(src)) { + off = (int)sizeof(src) - 1; + } + } off += snprintf(src + off, sizeof(src) - off, " W w0;\n"); off += snprintf(src + off, sizeof(src) - off, " w0.m();\n"); off += snprintf(src + off, sizeof(src) - off, "}\n");