Fix CI on Ubuntu 26.04 runners; use string_view over raw string pointers

- Define _GLIBCXX_NO_ASSERTIONS: libstdc++ 15 enables assertions at -O0,
  which pulls __glibcxx_assert_fail (C++ runtime) into the programs.
- test_redir_qemu: boot the guest with -cpu max so the runner's glibc
  loader runs under TCG.
- find_program: std::string_view PATH splitting into a fixed buffer, no
  heap and no null snprintf destination (GCC -Wformat-truncation).
- manager: parse /proc/net/udp numbers with strtoul (cert-err34-c);
  walk /proc/<pid>/cmdline with std::string_view.
- ssurl: sanitise the tag through std::span.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Max Lv
2026-10-27 16:58:50 +08:00
co-authored by Claude
parent 90ea095efe
commit 0343aa20f2
5 changed files with 69 additions and 38 deletions
+4
View File
@@ -31,6 +31,10 @@ function(ss_project_options target)
# it when they are rewritten.
target_compile_options(${target} PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions -fno-rtti -fno-threadsafe-statics>)
# libstdc++ 15 enables its assertions at -O0, and they call
# __glibcxx_assert_fail from the C++ runtime, which the programs and
# libshadowsocks-c must not need (scripts/check_cxx_runtime.py).
target_compile_definitions(${target} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:_GLIBCXX_NO_ASSERTIONS>)
# GCC's C++ -Wall adds -Wsign-compare and -Wclass-memaccess, which C's
# -Wall does not have. Modules converted from C keep the warning set
# they were written against; ss_strict_warnings() turns both back on.
+14 -9
View File
@@ -44,6 +44,7 @@
#include <netdb.h>
#include <errno.h>
#include <new>
#include <string_view>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
@@ -115,13 +116,15 @@ udp_sender_uid(const struct sockaddr_storage *from, uid_t *uid)
if (fgets(line, sizeof(line), f) != NULL) { // header
while (!found && fgets(line, sizeof(line), f) != NULL) {
char local[64];
unsigned port;
unsigned long owner;
char port_hex[9];
char owner_dec[21];
// sl local_address rem_address st tx:rx tr:when retrnsmt uid
if (sscanf(line, " %*s %63[0-9A-Fa-f]:%x %*s %*s %*s %*s %*s %lu", local, &port, &owner) != 3
|| port != want_port) {
if (sscanf(line, " %*s %63[0-9A-Fa-f]:%8[0-9A-Fa-f] %*s %*s %*s %*s %*s %20[0-9]",
local, port_hex, owner_dec) != 3
|| strtoul(port_hex, NULL, 16) != want_port) {
continue;
}
unsigned long owner = strtoul(owner_dec, NULL, 10);
// The kernel prints each 32-bit word of the address as a host
// order integer; zeros mean the socket is unbound (wildcard).
uint8_t bytes[16];
@@ -1041,13 +1044,15 @@ is_managed_server(int pid)
char cmdline[4096];
size_t n = fread(cmdline, 1, sizeof(cmdline), f);
fclose(f);
for (size_t i = 0; i < n;) {
const char *arg = cmdline + i;
size_t len = strnlen(arg, n - i);
if (len == strlen("--manager-address") && memcmp(arg, "--manager-address", len) == 0) {
// NUL-separated arguments; the last one may be cut short.
std::string_view rest(cmdline, n);
while (!rest.empty()) {
std::string_view::size_type nul = rest.find('\0');
std::string_view arg(rest.data(), nul != std::string_view::npos ? nul : rest.size());
if (arg == "--manager-address") {
return true;
}
i += len + 1;
rest.remove_prefix(nul != std::string_view::npos ? nul + 1 : rest.size());
}
return false;
#else
+46 -26
View File
@@ -4,12 +4,16 @@
#include <stdlib.h>
#include <string.h>
#include "utils.h"
#include <algorithm>
#include <array>
#include <string_view>
#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#else
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <spawn.h>
@@ -118,32 +122,51 @@ ss_process_user(struct ss_process *process, const char *user)
/* The executable execve() runs: `program` itself if it names a path, or the
* first match on PATH, as posix_spawnp() would find it. */
static char *
find_program(const char *program)
using ProgramPath = std::array<char, PATH_MAX>;
static bool
set_path(ProgramPath &out, std::string_view dir, std::string_view program)
{
if (strchr(program, '/') != NULL) {
return copy_string(program);
std::string_view slash = dir.empty() ? "" : "/";
if (dir.size() + slash.size() + program.size() >= out.size()) {
return false;
}
const char *path = getenv("PATH");
if (path == NULL || *path == '\0') {
auto end = std::copy(dir.begin(), dir.end(), out.begin());
end = std::copy(slash.begin(), slash.end(), end);
end = std::copy(program.begin(), program.end(), end);
*end = '\0';
return true;
}
static std::string_view
env_value(const char *name)
{
const char *value = getenv(name);
return value != NULL ? std::string_view(value) : std::string_view();
}
static bool
find_program(std::string_view program, ProgramPath &out)
{
if (program.find('/') != std::string_view::npos) {
return set_path(out, {}, program);
}
std::string_view path = env_value("PATH");
if (path.empty()) {
path = "/usr/local/bin:/usr/bin:/bin";
}
size_t room = strlen(path) + strlen(program) + 2;
char *candidate = (char *)ss_malloc(room); // heap-ok: plugin start, startup
for (const char *dir = path;; ) {
const char *colon = strchr(dir, ':');
size_t len = colon != NULL ? (size_t)(colon - dir) : strlen(dir);
snprintf(candidate, room, "%.*s%s%s", (int)len, dir, len > 0 ? "/" : "", program);
if (access(candidate, X_OK) == 0) {
return candidate;
for (;;) {
// Not substr(): it can throw, which would pull in the C++ runtime.
std::string_view::size_type colon = path.find(':');
std::string_view dir(path.data(), colon != std::string_view::npos ? colon : path.size());
if (set_path(out, dir, program) && access(out.data(), X_OK) == 0) {
return true;
}
if (colon == NULL) {
break;
if (colon == std::string_view::npos) {
return false;
}
dir = colon + 1;
path.remove_prefix(colon + 1);
}
free(candidate); // heap-ok: plugin start, startup
return NULL;
}
/* In the forked child: lose root, but keep the right to bind ports below
@@ -180,16 +203,14 @@ drop_in_child(const struct ss_process *process)
static int
fork_as_user(struct ss_process *process, char **env)
{
char *path = find_program(process->program);
if (path == NULL) {
ProgramPath path;
if (!find_program(process->program, path)) {
return ENOENT;
}
/* The child reports a failed switch or exec through this pipe. */
int report[2];
if (pipe(report) != 0) {
int error = errno;
free(path); // heap-ok: plugin start, startup
return error;
return errno;
}
(void)fcntl(report[1], F_SETFD, FD_CLOEXEC);
pid_t pid = fork();
@@ -204,7 +225,7 @@ fork_as_user(struct ss_process *process, char **env)
signal(SIGINT, SIG_DFL);
int error = EPERM;
if (drop_in_child(process) == 0) {
execve(path, process->argv, env); // NOLINT(clang-analyzer-optin.taint.GenericTaint): the PATH lookup posix_spawnp() would do
execve(path.data(), process->argv, env); // NOLINT(clang-analyzer-optin.taint.GenericTaint): the PATH lookup posix_spawnp() would do
error = errno;
}
ssize_t ignored = write(report[1], &error, sizeof(error));
@@ -227,7 +248,6 @@ fork_as_user(struct ss_process *process, char **env)
}
}
close(report[0]);
free(path); // heap-ok: plugin start, startup
return error;
}
+4 -3
View File
@@ -29,6 +29,7 @@
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <span>
#include "base64.h"
#include "ssurl.h"
@@ -253,9 +254,9 @@ ss_url_parse(const char *uri, ss_url_t *url)
url->tag = percent_decode(hash + 1, body_len - (size_t)(hash - body) - 1);
/* It is only logged: no control characters (newlines, escapes)
* from a link may reach the log. */
for (char *c = url->tag; *c != '\0'; c++) {
if ((unsigned char)*c < 0x20 || *c == 0x7f) {
*c = '?';
for (char &c : std::span<char>(url->tag, strlen(url->tag))) {
if ((unsigned char)c < 0x20 || c == 0x7f) {
c = '?';
}
}
body_len = (size_t)(hash - body);
+1
View File
@@ -222,6 +222,7 @@ SERIAL_LOG="$WORK_DIR/serial.log"
qemu-system-x86_64 \
$KVM_FLAG \
-cpu max \
-m 512 \
-kernel "$WORK_DIR/vmlinuz" \
-initrd "$WORK_DIR/initramfs.gz" \