-
Notifications
You must be signed in to change notification settings - Fork 1.1k
test: verify label-selected TAP ASAN CI #6083
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3.0
Are you sure you want to change the base?
Changes from all commits
a0aa90f
a8db282
0959fa9
8992620
7546d06
47d23e4
e537e90
e73ac41
dc53e74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Temporary CI ASAN validation probe | ||
|
|
||
| This disposable file opens a label-controlled CI validation pull request. | ||
| Markdown-only changes are ignored by `CI-trigger`; a subsequent non-ignored | ||
| no-op change will start the selected run after the `ci:asan` label is applied. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # PgSQL Poisoned-Query ASAN UAF Implementation Plan | ||
|
|
||
| **Goal:** Keep the incoming simple-query packet alive until request logging and | ||
| parser cleanup finish, then verify the complete ASAN TAP fan-out on PR #6083. | ||
|
|
||
| **Design:** `CurrentQuery` borrows its SQL pointer from the packet handled by | ||
| `handler_poisoned_simple_query()`. Preserve the existing ownership model and | ||
| move `RequestEnd()` before `l_free()` in both exits; do not add a copy or change | ||
| mirror-session behavior. | ||
|
|
||
| ## Implementation | ||
|
|
||
| 1. Use the existing failing ASAN run of | ||
| `pgsql-retry_guard_in_txn_on_broken_backend-t` as the regression's red state. | ||
| 2. Reorder finalization and packet release in both malformed and normal exits. | ||
| 3. Run formatting/diff checks and focused source checks, then commit and push | ||
| `ci/verify-asan-label` so the label-selected ASAN workflow reruns. | ||
| 4. Inspect all prior failed PR #6083 jobs, group failures by sanitizer signature, | ||
| and compare them against the fresh run before making any additional fix. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -264,6 +264,14 @@ | |
| if (opts.waitpid_delay_us != 0) to_opts.waitpid_delay_us = opts.waitpid_delay_us; | ||
| if (opts.sigkill_to_us != 0) to_opts.sigkill_to_us = opts.sigkill_to_us; | ||
|
|
||
| // Prepare argv before fork(). The child may run in a multi-threaded process | ||
| // where another thread held an allocator lock at the time of fork. | ||
| std::vector<const char*> child_argv {}; | ||
| child_argv.reserve(argv.size() + 2); | ||
| child_argv.push_back(file.c_str()); | ||
| child_argv.insert(child_argv.end(), argv.begin(), argv.end()); | ||
| child_argv.push_back(nullptr); | ||
|
|
||
| // Pipes for parent to write and read | ||
| int read_p_err = pipe(pipes[PARENT_READ_PIPE]); | ||
| int write_p_err = pipe(pipes[PARENT_WRITE_PIPE]); | ||
|
|
@@ -279,44 +287,22 @@ | |
| } | ||
|
|
||
| if(child_pid == 0) { | ||
| int child_err = 0; | ||
| std::vector<const char*> _argv = argv; | ||
|
|
||
| // Append null to end of _argv for extra safety | ||
| _argv.push_back(nullptr); | ||
| // Duplicate file argument to avoid manual duplication | ||
| _argv.insert(_argv.begin(), file.c_str()); | ||
|
|
||
| // close all files , with the exception of the pipes | ||
| close_all_non_term_fd({ CHILD_READ_FD, CHILD_WRITE_FD, CHILD_WRITE_ERR, PARENT_READ_FD, PARENT_READ_ERR, PARENT_WRITE_FD}); | ||
|
|
||
| // Copy the pipe descriptors | ||
| int dup_read_err = dup2(CHILD_READ_FD, STDIN_FILENO); | ||
| int dup_write_err = dup2(CHILD_WRITE_FD, STDOUT_FILENO); | ||
| int dup_err_err = dup2(CHILD_WRITE_ERR, STDERR_FILENO); | ||
|
|
||
| if (dup_read_err == -1 || dup_write_err == -1 || dup_err_err == -1) { | ||
| exit(errno); | ||
| _exit(errno); | ||
| } | ||
|
|
||
| // Close no longer needed pipes | ||
| close(CHILD_READ_FD); | ||
| close(CHILD_WRITE_FD); | ||
| close(CHILD_WRITE_ERR); | ||
|
|
||
| close(PARENT_READ_FD); | ||
| close(PARENT_READ_ERR); | ||
| close(PARENT_WRITE_FD); | ||
|
|
||
|
|
||
| char** args = const_cast<char**>(_argv.data()); | ||
| child_err = execvp(file.c_str(), args); | ||
| // The pipe endpoints now live on stdin/stdout/stderr. With no exclusions, | ||
| // descriptor cleanup can use close_range() (or its allocation-free fallback). | ||
| close_all_non_term_fd({}); | ||
|
|
||
| if (child_err) { | ||
| exit(errno); | ||
| } else { | ||
| exit(0); | ||
| } | ||
| char** args = const_cast<char**>(child_argv.data()); | ||
|
Check failure on line 303 in lib/proxysql_utils.cpp
|
||
| execvp(file.c_str(), args); | ||
| _exit(errno); | ||
|
Comment on lines
+303
to
+305
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect whether wexecvp callers require PATH lookup semantics.
rg -n -C 2 --glob '*.{cpp,h,hpp}' '\bwexecvp\s*\(' .Repository: sysown/proxysql Length of output: 6618 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- wexecvp implementation ---'
sed -n '220,315p' lib/proxysql_utils.cpp
printf '%s\n' '--- declaration and options ---'
sed -n '175,215p' include/proxysql_utils.h
printf '%s\n' '--- relevant test and caller contexts ---'
sed -n '90,125p' test/tap/tests/reg_test_5988-caching_sha2_rsa-t.cpp
sed -n '35,65p' test/tap/tests/frontend_x509_test_utils.h
sed -n '135,160p' lib/ProxySQL_RESTAPI_Server.cpp
sed -n '210,235p' test/tap/tests/cluster_sim_runner.h
printf '%s\n' '--- path construction and fork-related helpers ---'
rg -n -C 3 'fork\s*\(|close_all_non_term_fd|execvp|execv|execve|PATH' lib/proxysql_utils.cpp include/proxysql_utils.hRepository: sysown/proxysql Length of output: 18780 🌐 Web query:
💡 Result: In a multithreaded process, calling fork creates a child process containing only the single thread that invoked fork [1][2][3]. Because other threads are not duplicated, the child process may inherit inconsistent states for synchronization primitives (like mutexes) and other resources held by threads that no longer exist [1][4][5]. To avoid undefined behavior, deadlocks, or corruption, the POSIX standard requires that a child process only call async-signal-safe functions until it successfully calls one of the exec functions [1][2][4]. Regarding the specific functions requested: 1. execv and execve are categorized as async-signal-safe by POSIX [6][7]. 2. execvp (along with execlp and execvpe) is generally not considered async-signal-safe because it typically needs to search the PATH environment variable and may perform heap allocations, which are often not async-signal-safe [8][9]. While specific implementations may vary, relying on them as async-signal-safe is non-portable and violates standard safety guidelines [6][10]. 3. fork itself is listed as async-signal-safe by POSIX, but its use in a signal handler is highly discouraged and noted as a potential future removal from the safe list because of the complex interactions with multithreading and the potential for deadlock [7][11]. The complete list of POSIX-required async-signal-safe functions is defined in the POSIX.1 standard (often found in the signal-safety(7) manual page on Linux systems) [6][10]. Examples of safe functions include _exit, close, dup, dup2, execve, kill, read, write, and wait [6][7]. Functions that are not on this list are considered unsafe; using them in a signal handler or in a post-fork child before an exec call is undefined behavior [6][10]. For complex scenarios where cleanup is needed in the child before exec, developers should use pthread_atfork to register handlers, but even those handlers must only call async-signal-safe functions to remain compliant [1][4]. Citations:
Do not call Callers pass bare names such as 🤖 Prompt for AI AgentsSource: MCP tools |
||
| } else { | ||
| std::string stdout_ {}; | ||
| std::string stderr_ {}; | ||
|
|
@@ -531,16 +517,17 @@ | |
| * - This method is O(1) and the most efficient | ||
| * - ONLY used when excludeFDs is empty (otherwise would close excluded fds) | ||
| * | ||
| * 2. **Secondary Method:** Iterate through /proc/self/fd | ||
| * 2. **Secondary Method (non-empty excludeFDs only):** Iterate through /proc/self/fd | ||
| * - Uses opendir("/proc/self/fd") to get a directory stream of open file descriptors | ||
| * - Uses dirfd() to get the directory's own fd and skips closing it (prevents self-referential closure bug) | ||
| * - Reads each entry and uses atoi() to convert to fd (no heap allocation) | ||
| * - Closes all descriptors > 2 (stdin/stdout/stderr) that are not in the exclusion list | ||
| * - This method is O(n) where n is the number of open file descriptors | ||
| * | ||
| * 3. **Fallback Method:** Iterate through rlimit | ||
| * - If /proc/self/fd is not available (e.g., on non-Linux systems or chroot environments), | ||
| * falls back to getrlimit(RLIMIT_NOFILE) | ||
| * - For an empty excludeFDs list, this is used directly when close_range() is unavailable, | ||
| * because opendir() may allocate and is unsafe after a multi-threaded fork | ||
| * - For a non-empty list, this is used if /proc/self/fd is unavailable | ||
| * - Iterates from 3 to rlim_cur-1, attempting to close each descriptor | ||
| * - Ignores EBADF errors for descriptors that aren't actually open | ||
| * - This method is O(rlim_cur) which can be much slower if rlim_max is large (e.g., 1048576) | ||
|
|
@@ -558,10 +545,10 @@ | |
| * - This prevents undefined behavior from closing the fd while iterating | ||
| * | ||
| * **Thread Safety Considerations:** | ||
| * - This function IS safe to call in the child process between fork() and execve() | ||
| * - By avoiding heap allocations (using atoi() and simple loops), it prevents deadlocks | ||
| * on malloc locks that may be held by other threads in the parent at fork time | ||
| * - For optimal safety, call with an empty excludeFDs initializer list: close_all_non_term_fd({}) | ||
| * - With an empty excludeFDs list, this function is safe to call in the child process | ||
| * between fork() and execve(): both close_range() and the rlimit fallback avoid allocation | ||
| * - A non-empty excludeFDs list may use opendir() and should not be used after a | ||
| * multi-threaded fork | ||
| * | ||
| * **Parameters:** | ||
| * @param excludeFDs A vector of file descriptor numbers to keep open (in addition to 0, 1, 2) | ||
|
|
@@ -603,8 +590,13 @@ | |
| static int close_range_available = -1; // -1 = unknown, 0 = not available, 1 = available | ||
| if (close_range_available == 1) { | ||
| // close_range is available, use it to close all fds >= 3 | ||
| syscall(__NR_close_range, 3, ~0U, 0); | ||
| return; | ||
| long ret = syscall(__NR_close_range, 3, ~0U, 0); | ||
| if (ret == 0) { | ||
| return; | ||
| } | ||
| if (errno == ENOSYS) { | ||
| close_range_available = 0; | ||
| } | ||
| } | ||
| if (close_range_available == -1) { | ||
| // First call: check if close_range is available | ||
|
|
@@ -622,6 +614,19 @@ | |
| } | ||
| #endif | ||
|
|
||
| // For an empty exclusion list, callers can be in the child of a | ||
| // multi-threaded fork. Avoid opendir(), which may allocate internally. | ||
| if (excludeFDs.empty()) { | ||
| struct rlimit nlimit; | ||
| int rc = getrlimit(RLIMIT_NOFILE, &nlimit); | ||
| if (rc == 0) { | ||
| for (rlim_t fd_rlim = 3; fd_rlim < nlimit.rlim_cur && fd_rlim <= INT_MAX; fd_rlim++) { | ||
| close(static_cast<int>(fd_rlim)); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Fallback: iterate through /proc/self/fd | ||
| DIR *d; | ||
| struct dirent *dir; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| #!/bin/bash | ||
|
|
||
| # Return success only when the supplied ELF binary has a dynamic dependency on | ||
| # libasan. The central build hands binaries to separate TAP workflows, so build | ||
| # flags are no longer available when the test infrastructure starts. | ||
| proxysql_binary_uses_asan() { | ||
| local binary="${1:-}" | ||
| local dynamic_section | ||
|
|
||
| [ -r "${binary}" ] || return 1 | ||
|
|
||
| if command -v readelf >/dev/null 2>&1; then | ||
| dynamic_section="$(LC_ALL=C readelf --dynamic "${binary}" 2>/dev/null)" || return 1 | ||
| grep -Eq '\(NEEDED\).*\[libasan\.so(\.[^]]*)?\]' <<< "${dynamic_section}" | ||
| else | ||
| # proxysql's GCC ASAN build links libasan dynamically. Keep a | ||
| # dependency-free fallback for minimal runner hosts without binutils. | ||
| LC_ALL=C grep -aEq 'libasan\.so(\.[0-9]+)+' "${binary}" | ||
| fi | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| #!/bin/bash | ||
| set -euo pipefail | ||
|
|
||
| script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| subject="${script_dir}/asan-detection.bash" | ||
| tmp_dir="$(mktemp -d)" | ||
| trap 'rm -rf "${tmp_dir}"' EXIT | ||
|
|
||
| printf '%s\n' 'int main() { return 0; }' > "${tmp_dir}/main.cpp" | ||
| "${CXX:-c++}" "${tmp_dir}/main.cpp" -o "${tmp_dir}/plain" | ||
| "${CXX:-c++}" -fsanitize=address "${tmp_dir}/main.cpp" -o "${tmp_dir}/asan" | ||
|
|
||
| source "${subject}" | ||
|
|
||
| if proxysql_binary_uses_asan "${tmp_dir}/plain"; then | ||
| echo "plain binary incorrectly detected as ASAN" >&2 | ||
| exit 1 | ||
| fi | ||
| if ! proxysql_binary_uses_asan "${tmp_dir}/asan"; then | ||
| echo "ASAN binary was not detected" >&2 | ||
| exit 1 | ||
| fi | ||
| if proxysql_binary_uses_asan "${tmp_dir}/missing"; then | ||
| echo "missing binary incorrectly detected as ASAN" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "ASAN binary detection tests passed" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,7 +131,16 @@ ifneq ($(wildcard $(SQLITE3_LDIR)/vec.o),) | |
| endif | ||
| endif | ||
|
|
||
| OPT := $(STDCPP) -O2 -ggdb $(WGCOV) $(WASAN) -DGITVERSION=\"$(GIT_VERSION)\" | ||
| # Keep public class layouts identical to libproxysql.a. In particular, | ||
| # PROXYSQL40 adds members to ProxySQL_GlobalVariables; omitting the define in | ||
| # TAP translation units makes their GloVars allocation smaller than the | ||
| # constructor linked from the archive. | ||
| PSQL40 := | ||
| ifeq ($(PROXYSQL40),1) | ||
| PSQL40 := -DPROXYSQL40 | ||
| endif | ||
|
|
||
| OPT := $(STDCPP) -O2 -ggdb $(PSQL40) $(WGCOV) $(WASAN) -DGITVERSION=\"$(GIT_VERSION)\" | ||
|
Comment on lines
+134
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ifneq ($(UNAME_S),Darwin) | ||
| OPT += -Wl,--no-as-needed | ||
| endif | ||
|
|
@@ -159,7 +168,7 @@ tests_no_infra: admin_set_credentials_logging-t listener_conflicts_validation-t | |
| ./admin_set_credentials_logging-t | ||
| ./listener_conflicts_validation-t | ||
|
|
||
| DEBUG_OPT := $(STDCPP) -O0 -DDEBUG -ggdb $(WGCOV) $(WASAN) -DGITVERSION=\"$(GIT_VERSION)\" | ||
| DEBUG_OPT := $(STDCPP) -O0 -DDEBUG -ggdb $(PSQL40) $(WGCOV) $(WASAN) -DGITVERSION=\"$(GIT_VERSION)\" | ||
| ifneq ($(UNAME_S),Darwin) | ||
| DEBUG_OPT += -Wl,--no-as-needed | ||
| endif | ||
|
|
@@ -416,7 +425,7 @@ prepare_statement_err3024_async-t: prepare_statement_err3024-t.cpp $(TAP_LDIR)/l | |
|
|
||
| ifneq ($(UNAME_S),Darwin) | ||
| test_wexecvp_syscall_failures-t: test_wexecvp_syscall_failures-t.cpp $(TAP_LDIR)/libtap$(SHLIB_EXT) | ||
| $(CXX) $< $(IDIRS) $(LDIRS) $(OPT) $(MYLIBS) -Wl,--wrap=pipe,--wrap=fcntl,--wrap=read,--wrap=poll $(STATIC_LIBS) -o $@ | ||
| $(CXX) $< $(IDIRS) $(LDIRS) $(OPT) $(MYLIBS) -Wl,--wrap=pipe,--wrap=fcntl,--wrap=read,--wrap=poll,--wrap=fork,--wrap=_Znwm,--wrap=opendir $(STATIC_LIBS) -o $@ | ||
| endif | ||
|
|
||
| # Every test that links pg_lite_client.cpp shares one link line. pg_lite_client | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Name the exact non-ignored trigger.
The procedure requires a later non-ignored no-op change, but it does not identify the intended
.gitignorechange. Reference.gitignoreLine 244 so operators do not create an unrelated change.Proposed documentation update
📝 Committable suggestion
🤖 Prompt for AI Agents