[
{
"id": "shell-injection-string-concat",
"title": "Shell injection via string concatenation",
"severity": "critical",
"scope": "scheme",
"pattern": "(##shell-command|shell-command)\\s.*?(string-append|string-concatenate|format\\s)",
"message": "Shell command constructed via string concatenation. User-controlled input may allow arbitrary command execution.",
"remediation": "Use a safe process API (e.g., run-process with explicit argument lists) instead of shell-command with concatenated strings.",
"tags": [
"shell",
"injection",
"command",
"string-append",
"rce"
]
},
{
"id": "ffi-pointer-void-u8vector",
"title": "FFI (pointer void) likely passed a u8vector",
"severity": "high",
"scope": "ffi-boundary",
"pattern": "c-lambda\\s+\\([^)]*pointer\\s+void[^)]*\\)",
"message": "c-lambda uses (pointer void) parameter type. If callers pass u8vector values, Gambit will pass the raw Scheme object address (including header) instead of the data bytes, causing data corruption.",
"remediation": "Use scheme-object parameter type and access data bytes via ___BODY(arg) or U8_DATA() macro in the C body.",
"related_recipe": "ffi-scheme-object-u8vector-data",
"tags": [
"ffi",
"pointer",
"void",
"u8vector",
"type-mismatch",
"data-corruption"
]
},
{
"id": "alloc-scmobj-no-fixnump-check",
"title": "___alloc_scmobj without ___FIXNUMP failure check",
"severity": "high",
"scope": "c-shim",
"pattern": "___alloc_scmobj",
"message": "___alloc_scmobj can return a fixnum error code on allocation failure. Without a ___FIXNUMP check, the code will dereference a non-pointer, causing a crash.",
"remediation": "Check the return value with: if (___FIXNUMP(result)) { /* handle allocation failure */ }",
"tags": [
"alloc",
"scmobj",
"fixnump",
"oom",
"crash",
"gambit",
"c"
]
},
{
"id": "port-open-no-unwind-protect",
"title": "Port opened without unwind-protect resource guard",
"severity": "medium",
"scope": "scheme",
"pattern": "(open-input-file|open-output-file|open-input-string|open-output-string|open-tcp-client|open-tcp-server|open-process)",
"message": "File/network port opened without unwind-protect or call-with-* guard. If an exception occurs before close-port, the port leaks.",
"remediation": "Wrap in (call-with-input-file path proc) or (unwind-protect (begin ... (close-port p)) (close-port p)).",
"tags": [
"port",
"file",
"resource-leak",
"unwind-protect",
"exception"
]
},
{
"id": "c-buffer-silent-truncation",
"title": "C buffer memcpy with clamped size (silent truncation)",
"severity": "medium",
"scope": "c-shim",
"pattern": "memcpy\\s*\\([^;]*\\bmin\\b[^;]*\\)",
"message": "memcpy with min/clamp on size silently truncates data without reporting the truncation to the caller. This can cause data corruption or incomplete operations.",
"remediation": "Return the actual bytes copied or an error code when the buffer is too small, rather than silently truncating.",
"tags": [
"memcpy",
"truncation",
"buffer",
"silent",
"c"
]
},
{
"id": "static-global-buffer-thread-safety",
"title": "Static global buffer in C shim (thread safety)",
"severity": "medium",
"scope": "c-shim",
"pattern": "static\\s+(char|uint8_t|unsigned\\s+char)\\s+\\w+\\s*\\[",
"message": "Static buffer in C shim is shared across all threads. Concurrent calls from multiple Gerbil threads will cause data races.",
"remediation": "Use thread-local storage (__thread or pthread_key), allocate on the heap, or pass a caller-provided buffer.",
"tags": [
"static",
"buffer",
"thread",
"race",
"c",
"global"
]
},
{
"id": "unused-security-parameter",
"title": "Unused security-relevant function parameter",
"severity": "low",
"scope": "scheme",
"pattern": "\\(def\\s+\\([^)]*\\b(mode|mask|perm|permission|auth|token|secret|credential|password)\\b",
"message": "Function parameter with a security-relevant name (mode, mask, perm, etc.) may be intentionally ignored. Verify the parameter is actually used in the function body.",
"remediation": "Ensure the security-relevant parameter is used. If intentionally unused, prefix with underscore (_mode) to document the intent.",
"tags": [
"unused",
"parameter",
"permission",
"mode",
"heuristic"
]
},
{
"id": "ffi-void-return-on-init",
"title": "FFI init/initialize function declared as void instead of int",
"severity": "medium",
"scope": "ffi-boundary",
"pattern": "define-c-lambda\\s+\\S*_?[iI]nitialize\\s+\\([^)]*\\)\\s+void",
"message": "C library initialization functions typically return an error code (int), but this FFI binding declares void return type. If initialization fails (e.g., out of memory), the failure is silently ignored, leading to undefined behavior or crashes in subsequent calls.",
"remediation": "Change return type from void to int, and check the return value in the Scheme caller: (let ((r (lib_initialize obj))) (when (zero? r) (error \\\"Failed to initialize\\\")))",
"related_recipe": "ffi-initialize-return-check",
"tags": [
"ffi",
"initialize",
"void",
"return-type",
"error-check",
"init"
]
},
{
"id": "ffi-null-pointer-dereference",
"title": "FFI c-lambda dereferences pointer argument without null check",
"severity": "high",
"scope": "ffi-boundary",
"pattern": "___arg\\d+\\)\\s*->",
"message": "C code in c-lambda dereferences a pointer argument (___argN->field) without checking for NULL. If the Gambit pointer type allows NULL (which it does by default), this causes a segfault when NULL is passed. Common in FFI accessor functions for linked list traversal (next, children, properties, ns fields).",
"remediation": "Add a NULL check before dereferencing: '___return(___arg1 ? ((xmlNodePtr)___arg1)->field : ___FAL);' or ensure all call sites in the .ss wrapper guard with a truthy check before calling the accessor.",
"tags": [
"ffi",
"null",
"pointer",
"dereference",
"segfault",
"c-lambda",
"accessor"
]
},
{
"id": "ffi-mutex-no-unwind-protect",
"title": "Port mutex lock without unwind-protect risks deadlock on exception",
"severity": "medium",
"scope": "scheme",
"pattern": "macro-port-mutex-lock!",
"message": "Code acquires a port mutex via macro-port-mutex-lock! but may not release it if an exception occurs between lock and unlock. This causes a permanent deadlock on the port.",
"remediation": "Wrap the critical section between lock and unlock with unwind-protect or dynamic-wind to ensure the mutex is always released. See recipe: mutex-unwind-protect-deadlock-prevention.",
"related_recipe": "mutex-unwind-protect-deadlock-prevention",
"tags": [
"mutex",
"port",
"deadlock",
"unwind-protect",
"exception",
"lock"
]
},
{
"id": "ffi-scheme-object-stored-in-c-struct",
"title": "Scheme object stored in C struct without GC protection",
"severity": "high",
"scope": "c-shim",
"pattern": "___SCMOBJ\\s+\\w+;|->data\\s*=\\s*___arg",
"message": "A raw ___SCMOBJ (Scheme object handle) is stored in a C struct field. If this struct is used across GC safe points or in multithreaded code, the GC may relocate the object, making the stored handle point to stale memory. This causes silent data corruption or segfaults.",
"remediation": "Either: (1) copy the Scheme data to a C-allocated buffer before storing, (2) use ##still-copy to pin the object, or (3) ensure the code path is single-threaded and contains no GC safe points (no Scheme allocations).",
"related_recipe": "ffi-scheme-object-gc-safety-long-running",
"tags": [
"ffi",
"gc",
"scheme-object",
"SCMOBJ",
"struct",
"stale",
"multithreaded"
]
},
{
"id": "dbi-connection-method-close",
"title": "DBI connection closed via method dispatch instead of sql-close",
"severity": "medium",
"scope": "scheme",
"pattern": "\\{close\\s+\\((?:Kvs-connection|connection-e|.*-connection)\\s",
"message": "DBI connection objects (from :std/db/dbi) do NOT have a {close} method. Using method dispatch {close conn} on a DBI connection raises a contract violation that may be silently caught, leaving the connection open. With SQLite EXCLUSIVE locking, this prevents the database from being reopened and causes 'database is locked' errors.",
"remediation": "Use (sql-close conn) from :std/db/dbi instead of {close conn}. Also finalize any prepared statements with (sql-finalize stmt) before closing the connection. See cookbook recipe 'dbi-connection-close-sql-close'.",
"related_recipe": "dbi-connection-close-sql-close",
"tags": [
"dbi",
"connection",
"close",
"sqlite",
"resource-leak",
"method-dispatch",
"database-locked"
]
},
{
"id": "blst-serialize-buffer-overflow",
"title": "BLST serialize/deserialize used instead of compress/uncompress — buffer overflow",
"severity": "critical",
"scope": "c-shim",
"pattern": "blst_p[12]_affine_serialize|blst_p[12]_deserialize",
"message": "blst_p1_affine_serialize writes 96 bytes and blst_p2_affine_serialize writes 192 bytes (uncompressed format). If the output buffer is sized for compressed format (48/96 bytes), this causes a buffer overflow. Similarly, blst_p1_deserialize reads 96 bytes and blst_p2_deserialize reads 192 bytes — if the input buffer is smaller, this is an out-of-bounds read.",
"remediation": "Use blst_p1_affine_compress (48 bytes) / blst_p1_uncompress instead for standard BLS12-381 compressed format. Or use blst_p2_affine_compress (96 bytes) / blst_p2_uncompress for G2 points. Only use serialize/deserialize if buffers are sized for uncompressed format (96/192 bytes).",
"related_recipe": "blst-compress-uncompress-not-serialize",
"tags": [
"blst",
"bls",
"buffer-overflow",
"serialize",
"compress",
"crypto"
]
},
{
"id": "ffi-void-return-hides-errors",
"title": "FFI c-lambda declares void return type for C function that returns int — errors silently ignored",
"severity": "medium",
"scope": "ffi-boundary",
"pattern": "define-c-lambda\\s+\\S+\\s+\\([^)]*\\)\\s+void\\s",
"message": "A c-lambda declared with void return type discards the C function's return value. If the C function returns an error code (int), failures are silently ignored. This is especially dangerous for crypto/hash/init functions where errors indicate corruption or misconfiguration.",
"remediation": "Check the C function's actual return type. If it returns int (error code), declare the c-lambda return type as int and check the result in the Scheme wrapper (e.g. (unless (zero? result) (error ...))).",
"related_recipe": "ffi-initialize-return-check",
"tags": [
"ffi",
"void",
"return",
"error",
"silent",
"c-lambda"
]
},
{
"id": "blst-success-check-wrong-value",
"title": "BLST return value checked against 1 instead of 0 (BLST_SUCCESS = 0)",
"severity": "critical",
"scope": "scheme",
"pattern": "\\(=\\s+1\\s+.*blst|blst.*\\(=\\s+1\\s",
"message": "BLST API functions return BLST_SUCCESS (0) on success, not 1. Checking (= 1 result) causes valid operations to appear as failures and may accept some invalid inputs. This affects blst_core_verify_pk_in_g1, blst_p1_deserialize, blst_p2_deserialize, and other BLST functions.",
"remediation": "Use (zero? result) to check for BLST_SUCCESS, not (= 1 result). The BLST error enum: BLST_SUCCESS=0, BLST_BAD_ENCODING=1, BLST_POINT_NOT_ON_CURVE=2, etc.",
"related_recipe": "blst-compress-uncompress-not-serialize",
"tags": [
"blst",
"bls",
"verify",
"return-value",
"BLST_SUCCESS",
"crypto"
]
},
{
"id": "shell-injection-format-sh",
"title": "Shell injection via format string passed to /bin/sh -c",
"severity": "critical",
"scope": "scheme",
"pattern": "\"(/bin/sh|/bin/bash)\".*\"-c\".*format\\s",
"message": "Using (format ...) to build a shell command string passed to /bin/sh -c allows shell injection if any interpolated value contains shell metacharacters (;, &&, |, $, backticks, etc). User-controlled input in the format arguments can execute arbitrary commands.",
"remediation": "Use run-process or run-process/batch with an argument list instead of invoking a shell. Pass each argument as a separate list element: (run-process/batch [\"cmd\" \"arg1\" arg2-variable]). This bypasses shell interpretation entirely.",
"related_recipe": "safe-run-process-no-shell",
"tags": [
"shell",
"injection",
"format",
"sh",
"bash",
"command",
"run-process"
]
},
{
"id": "shell-injection-string-append-sh",
"title": "Shell injection via string-append passed to /bin/sh -c",
"severity": "critical",
"scope": "scheme",
"pattern": "\"(/bin/sh|/bin/bash)\".*\"-c\".*string-append",
"message": "Using string-append to build a shell command string passed to /bin/sh -c allows shell injection if any concatenated value contains shell metacharacters. This is the string-append variant of the format-based injection pattern.",
"remediation": "Use run-process or run-process/batch with an argument list instead of invoking a shell. Pass each argument as a separate list element: (run-process/batch [\"cmd\" \"arg1\" arg2-variable]).",
"related_recipe": "safe-run-process-no-shell",
"tags": [
"shell",
"injection",
"string-append",
"sh",
"bash",
"command"
]
},
{
"id": "c-shim-static-string-buffer-thread-safety",
"title": "Static string return buffer in C shim is not thread-safe",
"severity": "medium",
"scope": "c-shim",
"pattern": "static\\s+char\\s+\\w+\\[.*\\]\\s*;",
"message": "Static char buffer used for returning strings from C to Scheme is shared across all threads. If multiple OS threads call FFI functions that use this buffer concurrently, data races can corrupt returned strings. This is safe for single-threaded Qt event loop usage but dangerous in multi-threaded actor/channel programs.",
"remediation": "Use thread_local storage (C11: _Thread_local, C++11: thread_local) for the return buffer, or allocate per-call with malloc and document ownership. Example: static thread_local char s_return_buf[4096];",
"related_recipe": "ffi-c-shim-static-return-buffer",
"tags": [
"static",
"buffer",
"thread-safety",
"string",
"return",
"data-race",
"c-shim"
]
},
{
"id": "c-declare-ifdef-null-stub",
"title": "c-declare #ifdef/#else stub returning NULL pointer — segfault in cross-project exe builds",
"severity": "critical",
"scope": "ffi-boundary",
"pattern": "return\\s*\\(\\s*\\(?\\s*void\\s*\\*\\s*\\)?\\s*\\)?\\s*0\\s*;|return\\s+NULL\\s*;|return\\s+0\\s*;",
"message": "c-declare block contains a stub function returning NULL/0. When Gambit builds a downstream exe, per-module cc-options are NOT propagated, so #ifdef guards evaluate incorrectly and these stubs compile instead of the real implementation. This causes NULL pointer dereferences (segfaults) at runtime.",
"remediation": "Remove the #else stub entirely. Let the C compiler/linker error at build time if the library isn't available. For optional features, detect availability in build.ss and conditionally include the module rather than using #ifdef stubs in c-declare blocks. See cookbook recipe: c-declare-ifdef-stubs-null-pointer-danger",
"related_recipe": "c-declare-ifdef-stubs-null-pointer-danger",
"tags": [
"c-declare",
"ifdef",
"NULL",
"stub",
"segfault",
"cross-project",
"exe",
"cc-options"
]
},
{
"id": "c-declare-static-function-shadows-extern",
"title": "Static function in c-declare shadows external library function",
"severity": "high",
"scope": "ffi-boundary",
"pattern": "static\\s+\\w[\\w\\s\\*]*\\w+\\s*\\([^)]*\\)\\s*\\{[^}]*return\\s+(NULL|0|\\(void\\s*\\*\\)\\s*0)",
"message": "A static function stub in a c-declare block will shadow any external function with the same name from linked libraries. In C, static functions have internal linkage and take precedence over external symbols in the same compilation unit. This means even if the real library is linked, the stub's return value (typically NULL/0) will be used instead.",
"remediation": "Never define static stub functions in c-declare blocks. Either include the real header file unconditionally (let the linker fail if the library is missing), or use build.ss conditional module inclusion to avoid compiling the file at all when the library is unavailable.",
"related_recipe": "c-declare-ifdef-stubs-null-pointer-danger",
"tags": [
"static",
"shadow",
"extern",
"c-declare",
"linkage",
"function",
"stub"
]
},
{
"id": "sci-send-string-missing-wparam",
"title": "sci-send/string called without explicit wParam for length-dependent Scintilla messages",
"severity": "high",
"scope": "scheme",
"pattern": "sci-send/string\\s+\\S+\\s+SCI_REPLACETARGET\\s+\\S+\\s*\\)",
"message": "sci-send/string defaults wParam to 0. For SCI_REPLACETARGET, wParam=0 means 'replace with 0 bytes' which silently deletes the entire target region instead of replacing it. Similar issue affects SCI_APPENDTEXT and SCI_SEARCHINTARGET.",
"remediation": "Pass the string length as the fourth argument: (sci-send/string ed SCI_REPLACETARGET text (string-length text)). For Unicode text, use string-utf8-length for byte-accurate length.",
"related_recipe": "qt-sci-send-string-wparam-length",
"tags": [
"scintilla",
"sci-send/string",
"wparam",
"SCI_REPLACETARGET",
"data-loss",
"silent-deletion",
"qt"
]
},
{
"id": "pregexp-unsupported-inline-flag",
"title": "Unsupported pregexp inline flag (?i)/(?m)/(?s) causes runtime crash",
"severity": "high",
"scope": "scheme",
"pattern": "pregexp[^\\\"]*\\\"[^\\\"]*\\(\\?[ims]",
"message": "std/pregexp does not support PCRE inline flags like (?i), (?m), or (?s). Using them causes a runtime crash: [BUG]: pregexp internal error in pregexp-read-cluster-type. This is not caught at compile time.",
"remediation": "Remove the inline flag and either: (1) downcase the input string with string-downcase before matching, then use a plain lowercase pattern; or (2) use character classes like [Aa][Bb] for case-insensitive matching. Also avoid POSIX bracket expressions like [[:space:]] — use \\\\s instead.",
"related_recipe": "pregexp-no-inline-flags",
"tags": [
"pregexp",
"regex",
"inline-flag",
"case-insensitive",
"runtime-crash",
"gotcha"
]
},
{
"id": "c-shim-array-no-bounds-check",
"title": "C shim array access without bounds validation",
"severity": "high",
"scope": "c-shim",
"pattern": "ov\\[.*idx|arr\\[.*idx|buf\\[.*idx|table\\[.*index",
"message": "C shim accesses array using an index parameter from Scheme without bounds checking. Scheme callers can pass arbitrary index values, causing out-of-bounds memory reads (information leak) or writes (code execution). Always validate idx against the array's known size before access.",
"remediation": "Add bounds check before array access: get the array size (e.g. pcre2_get_ovector_count), check `if (idx >= count) return SENTINEL;`, then access. Return a safe sentinel value (PCRE2_UNSET, 0, NULL, 1 for bool) for out-of-bounds access.",
"related_recipe": "ffi-ovector-bounds-check",
"tags": [
"array",
"bounds-check",
"ovector",
"index",
"out-of-bounds",
"memory-safety"
]
},
{
"id": "c-shim-static-globals-no-mutex",
"title": "Static C globals without mutex protection for thread safety",
"severity": "medium",
"scope": "c-shim",
"pattern": "^static\\s+(int|size_t|char|uint32_t|PCRE2_SIZE)\\s+_ffi_",
"message": "Static C variables used to communicate results between FFI calls (e.g. error codes, offsets, buffers) are not thread-safe. Concurrent calls from multiple Gerbil threads will corrupt shared state, producing wrong error messages, offsets, or data. This is especially dangerous for error-reporting statics used during compilation or initialization.",
"remediation": "Protect access to static C globals with a Scheme-level mutex (mutex-lock!/unwind-protect/mutex-unlock!) around all FFI calls that read or write the statics. Alternatively, refactor to pass results via return values or caller-allocated buffers instead of statics.",
"tags": [
"static",
"global",
"thread-safety",
"mutex",
"concurrent",
"race-condition",
"ffi"
]
},
{
"id": "qt-process-status-sigchld-race",
"title": "process-status after open-process in Qt executable causes indefinite deadlock",
"severity": "high",
"scope": "scheme",
"pattern": "\\(process-status\\s+\\w",
"message": "Calling (process-status p) after (open-process ...) is unsafe in Qt executables. Qt's QCoreApplication installs a SIGCHLD handler that calls waitpid(-1, &status, WNOHANG) to reap all child zombies. If Qt's handler runs between Gambit's fork() and its waitpid(pid, ...) call in process-status, the child is reaped by Qt first, causing Gambit's waitpid to return ECHILD and block indefinitely. This produces a silent hang with no error message.",
"remediation": "In Qt executables (files compiled into exe targets that import :gerbil-qt/qt), omit (process-status p) after (read-line p #f). The read-line call already blocks until stdout EOF, which only occurs when the subprocess exits and closes its write pipe. The zombie process is then reaped by Qt's SIGCHLD handler. In TUI (non-Qt) executables, process-status is safe and should be retained.",
"related_recipe": "qt-open-process-no-process-status",
"tags": [
"qt",
"process-status",
"sigchld",
"waitpid",
"deadlock",
"hang",
"subprocess",
"race-condition"
]
},
{
"id": "ffi-string-length-as-byte-count",
"title": "string-length passed as byte count to FFI C function",
"severity": "high",
"scope": "scheme",
"pattern": "\\(string-length\\s+\\w+\\).*\\bffi-|ffi-.*\\(string-length\\s+\\w+\\)",
"message": "string-length returns character count, not UTF-8 byte count. When passed to a C function via FFI (char-string encodes as UTF-8), non-ASCII strings will be truncated. For \"café\", string-length returns 4 but UTF-8 byte length is 5.",
"remediation": "Use PCRE2_ZERO_TERMINATED or strlen() in the C shim to let C compute the byte length. Alternatively, use (u8vector-length (string->bytes s)) on the Scheme side. Never pass (string-length s) as a byte-count argument to FFI functions.",
"related_recipe": "ffi-string-length-byte-length-mismatch",
"tags": [
"ffi",
"utf8",
"string-length",
"byte-length",
"truncation",
"char-string"
]
},
{
"id": "c-declare-static-buffer-no-thread",
"title": "Static C buffer in c-declare without __thread qualifier",
"severity": "medium",
"scope": "c-shim",
"pattern": "static\\s+char\\s+\\w+\\[(?!.*__thread)",
"message": "Static char buffers in c-declare blocks are shared across all Gambit threads. Concurrent FFI calls can corrupt the buffer contents, leading to garbled strings or crashes. This is especially dangerous for error message buffers used by multiple regex operations.",
"remediation": "Add the __thread qualifier to make the buffer thread-local: 'static __thread char buf[256]'. This gives each OS thread its own copy with no mutex overhead. Note: __thread requires C11 or GCC/Clang extension support.",
"related_recipe": "ffi-thread-local-static-buffer",
"tags": [
"thread-safety",
"static",
"buffer",
"c-declare",
"__thread",
"race-condition"
]
},
{
"id": "ffi-char-string-unicode-crash",
"title": "FFI char-string type crashes on non-Latin-1 Unicode",
"severity": "high",
"scope": "ffi-boundary",
"pattern": "c-lambda\\s+\\([^)]*char-string",
"message": "Gambit's char-string FFI type only handles ISO-8859-1 / Latin-1 (U+0000-U+00FF). Strings with characters above U+00FF (€, CJK, emoji, em dash, etc.) cause a runtime crash: \\\"Can't convert to C char-string\\\". This is especially dangerous when the library claims UTF-8 support.",
"remediation": "Replace char-string with UTF-8-string in c-lambda declarations. UTF-8-string converts Scheme strings to/from UTF-8 encoded C strings, supporting the full Unicode range. Example: (define-c-lambda foo (UTF-8-string) int \\\"foo\\\") instead of (define-c-lambda foo (char-string) int \\\"foo\\\").",
"related_recipe": "ffi-utf8-string-type",
"tags": [
"ffi",
"char-string",
"UTF-8-string",
"unicode",
"crash",
"latin-1",
"gambit"
]
},
{
"id": "read-subu8vector-after-read-line",
"title": "Mixing char I/O and byte I/O on same port",
"severity": "high",
"scope": "scheme",
"pattern": "read-subu8vector",
"message": "Using read-subu8vector (byte I/O) on a port that may also use read-line/read-char/read-string (char I/O) raises nonempty-input-port-character-buffer-exception in Gambit. The char reader consumes bytes into a character buffer; subsequent byte reads find the byte buffer empty and crash. This is especially dangerous in LSP/HTTP Content-Length framed protocols where headers are read with read-line and body with read-subu8vector.",
"remediation": "Use read-string instead of read-subu8vector when reading from a port that also uses character I/O (read-line, read-char). For ASCII content like JSON, Content-Length in bytes equals string-length in characters. Pattern: (let ((body (read-string content-length port))) ...)",
"related_recipe": "lsp-content-length-framing-stdio",
"tags": [
"gambit",
"port",
"read-subu8vector",
"read-line",
"char-buffer",
"byte-buffer",
"lsp",
"content-length"
]
},
{
"id": "silent-exception-swallow-in-thread",
"title": "Silent exception swallowing in background threads",
"severity": "medium",
"scope": "scheme",
"pattern": "\\(with-catch\\s+\\(lambda\\s+\\([^)]+\\)\\s+\\(void\\)\\)",
"message": "with-catch handler that returns (void) silently swallows ALL exceptions in background threads, making crashes appear as hangs. The thread runs, hits an error (e.g. nonempty-input-port-character-buffer-exception), and exits with no visible error. From the outside, the thread appears to never have run at all.",
"remediation": "Always log exceptions in background thread catch handlers, even if you expect the thread to be robust. At minimum: (with-catch (lambda (e) (displayln \"thread error: \" e) (force-output)) thunk). Consider using (current-error-port) for logging.",
"tags": [
"thread",
"with-catch",
"void",
"silent",
"exception",
"hang",
"debug"
]
},
{
"id": "ffi-open-missing-mode-arg",
"title": "C open() called with missing mode parameter via FFI",
"severity": "medium",
"scope": "scheme",
"pattern": "ffi-open-raw\\s+\\\"[^\\\"]+\\\"\\s+\\d+\\s*\\)",
"message": "ffi-open-raw wraps C's open(path, flags, mode) which is declared as 3-arity (char-string int int). Calling with only 2 args (path and flags, missing mode) is undefined behavior — the third parameter picks up stack garbage. This typically 'works' for read-only opens of existing files on Linux but can cause silent corruption or crashes in other scenarios.",
"remediation": "Always pass all 3 arguments to ffi-open-raw: (ffi-open-raw path flags mode). For read-only opens use mode 0: (ffi-open-raw \"/dev/null\" 0 0). For file creation use appropriate permissions: (ffi-open-raw path (bitwise-ior O_WRONLY O_CREAT O_TRUNC) #o666).",
"related_recipe": "ffi-posix-syscall-begin-ffi",
"tags": [
"ffi",
"open",
"arity",
"mode",
"undefined-behavior",
"stack-garbage"
]
},
{
"id": "hardcoded-path-in-source",
"title": "Hardcoded filesystem paths leak build environment info in compiled binaries",
"severity": "low",
"scope": "scheme",
"pattern": "\"\\/home\\/|\"\\/opt\\/|\"\\/usr\\/local\\/|\"\\/tmp\\/",
"message": "Hardcoded absolute filesystem paths survive into compiled binaries and reveal build environment details (usernames, directory structure, installed software locations). These strings appear in .rodata and can be extracted with `strings`.",
"remediation": "Use relative paths, environment variables, or runtime path resolution instead of hardcoded absolute paths. For paths needed only at build time, use build.ss configuration rather than embedding in source. If paths must be absolute, consider using a path prefix variable that can be overridden.",
"related_recipe": "post-build-binary-obfuscation-pipeline",
"tags": [
"path",
"information-leak",
"binary",
"hardcoded",
"build-environment"
]
},
{
"id": "version-string-disclosure",
"title": "Version strings in source code reveal technology stack in compiled binaries",
"severity": "low",
"scope": "scheme",
"pattern": "\"[Gg]erbil|\"[Gg]ambit|\"[Ss]cheme|gerbil-version-string|gambit-version-string",
"message": "Explicit references to Gerbil, Gambit, or Scheme in string literals survive into compiled binaries. Combined with Gambit's own embedded version/configure strings, these make technology identification trivial for reverse engineers.",
"remediation": "Avoid embedding technology-identifying strings in production code. If version info is needed at runtime, gate it behind a debug/verbose flag or use a compile-time conditional. For maximum obfuscation, also patch Gambit's configure string and version info post-build.",
"related_recipe": "post-build-binary-obfuscation-pipeline",
"tags": [
"version",
"information-leak",
"binary",
"reverse-engineering",
"technology-stack"
]
}
]