Merge branch 'fix/issues-a3' into fix/issues-triage
This commit is contained in:
+158
-11
@@ -3,6 +3,7 @@
|
||||
#include "utils.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <fnmatch.h>
|
||||
#include <grp.h>
|
||||
#include <limits.h>
|
||||
#include <pwd.h>
|
||||
@@ -12,6 +13,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static bool identity_id_fits_int32(unsigned long id);
|
||||
|
||||
/* The active identity snapshot lives in a per-process global. The TCP server
|
||||
* forks one child process per connection, so a connection never shares this
|
||||
* with another; within a connection the multithreaded receiver reads it without
|
||||
@@ -419,17 +422,10 @@ static int identity_parse_from(const char* token, bool is_group, int32_t* out_fr
|
||||
/* Not a numeric LOW-HIGH range: fall through and treat as a name (a
|
||||
* hyphenated account name like "wayne-smith" must still resolve). */
|
||||
}
|
||||
/* A sender-side name. A wildcard other than the bare '*' is matched by rsync
|
||||
* against the sender's names; because FastSync transmits numeric ids only, the
|
||||
* receiver cannot evaluate it, so reject rather than silently mis-match. */
|
||||
if (identity_token_has_glob(token)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%smap FROM '%s': name wildcards other than '*' are not supported "
|
||||
"(FastSync transmits numeric ids, so sender names are unavailable on the "
|
||||
"receiver)",
|
||||
is_group ? "--group" : "--user", token);
|
||||
return -1;
|
||||
}
|
||||
/* A sender-side name. A FROM name wildcard other than the bare '*' is handled
|
||||
* by identity_expand_from_glob() in the caller (it expands against the
|
||||
* sender's account database at CLI-parse time), so this function only sees the
|
||||
* bare '*' or a literal name here. */
|
||||
int32_t id;
|
||||
if (identity_resolve_token(token, is_group, &id) != 0)
|
||||
return -1;
|
||||
@@ -486,6 +482,138 @@ static int identity_append_rule(IdentityMap** map, int* count, const IdentityMap
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* True when `lo` and `hi` are adjacent ids (no overflow at INT32_MAX). */
|
||||
static bool identity_ids_adjacent(int32_t lo, int32_t hi) {
|
||||
return lo < INT32_MAX && hi == lo + 1;
|
||||
}
|
||||
|
||||
static int identity_id_cmp(const void* a, const void* b) {
|
||||
int32_t x = *(const int32_t*)a;
|
||||
int32_t y = *(const int32_t*)b;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
static bool identity_ids_push(int32_t** ids, size_t* count, size_t* cap, int32_t id) {
|
||||
if (*count == *cap) {
|
||||
size_t grown_cap = *cap ? *cap * 2 : 16;
|
||||
int32_t* grown = realloc(*ids, grown_cap * sizeof(int32_t));
|
||||
if (!grown)
|
||||
return false;
|
||||
*ids = grown;
|
||||
*cap = grown_cap;
|
||||
}
|
||||
(*ids)[(*count)++] = id;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Expand a FROM name wildcard (rsync's match against sender-side account names)
|
||||
* into one rule per contiguous run of matching numeric ids, all sharing the same
|
||||
* TO side. FastSync transmits numeric ids only, so the wildcard must be
|
||||
* resolved here -- at CLI-parse time -- against the SENDER's passwd/group
|
||||
* database; the receiver has no sender names to match. Contiguous matched ids
|
||||
* are collapsed into a single LOW-HIGH range (a range of adjacent ids contains
|
||||
* exactly the ids it spans, so this is semantically exact). Returns 0 on
|
||||
* success, -1 on an allocation failure, a wildcard that matches no sender
|
||||
* account, or an expansion that would push the map past MAX_IDENTITY_MAP. */
|
||||
static int identity_expand_from_glob(Config* config, const char* glob, bool is_group,
|
||||
const IdentityMap* to_rule) {
|
||||
const char* optname = is_group ? "--groupmap" : "--usermap";
|
||||
size_t cap = 0;
|
||||
size_t n = 0;
|
||||
int32_t* ids = NULL;
|
||||
bool alloc_failed = false;
|
||||
|
||||
if (is_group) {
|
||||
setgrent();
|
||||
struct group* gr;
|
||||
while ((gr = getgrent()) != NULL) {
|
||||
if (fnmatch(glob, gr->gr_name, 0) != 0)
|
||||
continue;
|
||||
if (!identity_id_fits_int32((unsigned long)gr->gr_gid))
|
||||
continue;
|
||||
if (!identity_ids_push(&ids, &n, &cap, (int32_t)gr->gr_gid)) {
|
||||
alloc_failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
endgrent();
|
||||
} else {
|
||||
setpwent();
|
||||
struct passwd* pw;
|
||||
while ((pw = getpwent()) != NULL) {
|
||||
if (fnmatch(glob, pw->pw_name, 0) != 0)
|
||||
continue;
|
||||
if (!identity_id_fits_int32((unsigned long)pw->pw_uid))
|
||||
continue;
|
||||
if (!identity_ids_push(&ids, &n, &cap, (int32_t)pw->pw_uid)) {
|
||||
alloc_failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
endpwent();
|
||||
}
|
||||
|
||||
if (alloc_failed) {
|
||||
free(ids);
|
||||
log_message(LOG_LEVEL_ERROR, "%s: memory allocation failed expanding FROM '%s'", optname, glob);
|
||||
return -1;
|
||||
}
|
||||
if (n == 0) {
|
||||
free(ids);
|
||||
log_message(LOG_LEVEL_ERROR, "%s FROM '%s': no source account name matches the wildcard",
|
||||
optname, glob);
|
||||
return -1;
|
||||
}
|
||||
|
||||
qsort(ids, n, sizeof(int32_t), identity_id_cmp);
|
||||
size_t unique = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (unique == 0 || ids[unique - 1] != ids[i])
|
||||
ids[unique++] = ids[i];
|
||||
}
|
||||
n = unique;
|
||||
|
||||
int runs = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (i == 0 || !identity_ids_adjacent(ids[i - 1], ids[i]))
|
||||
runs++;
|
||||
}
|
||||
|
||||
IdentityMap** map = is_group ? &config->groupmap : &config->usermap;
|
||||
int* count = is_group ? &config->groupmap_count : &config->usermap_count;
|
||||
if (*count > MAX_IDENTITY_MAP - runs) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%s FROM '%s': the name wildcard expands to %d rule(s), which would exceed "
|
||||
"the maximum of %d map rules",
|
||||
optname, glob, runs, MAX_IDENTITY_MAP);
|
||||
free(ids);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < n;) {
|
||||
size_t j = i;
|
||||
while (j + 1 < n && identity_ids_adjacent(ids[j], ids[j + 1]))
|
||||
j++;
|
||||
IdentityMap rule;
|
||||
rule.from = ids[i];
|
||||
rule.from_hi = ids[j];
|
||||
rule.to = to_rule->to;
|
||||
rule.to_name = to_rule->to_name ? str_dup(to_rule->to_name) : NULL;
|
||||
if (to_rule->to_name && !rule.to_name) {
|
||||
free(ids);
|
||||
return -1;
|
||||
}
|
||||
if (identity_append_rule(map, count, &rule) != 0) {
|
||||
free(rule.to_name);
|
||||
free(ids);
|
||||
return -1;
|
||||
}
|
||||
i = j + 1;
|
||||
}
|
||||
free(ids);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int identity_parse_map(Config* config, const char* value, bool is_group) {
|
||||
if (!config || !value || *value == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "%smap requires a value", is_group ? "--group" : "--user");
|
||||
@@ -509,6 +637,25 @@ int identity_parse_map(Config* config, const char* value, bool is_group) {
|
||||
char* to_token = colon + 1;
|
||||
IdentityMap parsed;
|
||||
memset(&parsed, 0, sizeof(parsed));
|
||||
/* A FROM name wildcard (anything with a glob metacharacter other than the
|
||||
* bare '*') is expanded against the sender's account database here, while
|
||||
* the sender's passwd/group DB is still available; the resulting numeric
|
||||
* rules travel on the wire like an explicit list. The TO side is parsed
|
||||
* first so every expanded rule shares it. */
|
||||
if (strcmp(from_token, "*") != 0 && identity_token_has_glob(from_token)) {
|
||||
if (identity_parse_to(to_token, is_group, &parsed.to, &parsed.to_name) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "%s could not parse TO '%s' in '%s'", optname, to_token,
|
||||
value);
|
||||
free(list);
|
||||
return -1;
|
||||
}
|
||||
if (identity_expand_from_glob(config, from_token, is_group, &parsed) != 0) {
|
||||
free(parsed.to_name);
|
||||
free(list);
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (identity_parse_from(from_token, is_group, &parsed.from, &parsed.from_hi) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%s could not resolve FROM '%s' in '%s' (a name must exist on the "
|
||||
|
||||
@@ -25,8 +25,14 @@
|
||||
|
||||
/* Parse one --usermap= / --groupmap= value (comma-separated FROM:TO rules,
|
||||
* first match wins) into config->usermap / config->groupmap. is_group selects
|
||||
* the group tables and name databases. Returns 0 on success, -1 on a
|
||||
* malformed spec or an unresolvable name (never a silent no-op). */
|
||||
* the group tables and name databases. A FROM name wildcard (containing `*`,
|
||||
* `?` or `[...]`, but not the bare `*`) is expanded against the SENDER's
|
||||
* account database at parse time into one or more numeric id/range rules
|
||||
* (contiguous ids collapse to a range) sharing the same TO, because only
|
||||
* numeric ids cross the wire; the expansion is capped at MAX_IDENTITY_MAP and a
|
||||
* wildcard matching no account is an error. Returns 0 on success, -1 on a
|
||||
* malformed spec, an unresolvable name, an unmatched wildcard, or a map that
|
||||
* would exceed MAX_IDENTITY_MAP (never a silent no-op). */
|
||||
int identity_parse_map(Config* config, const char* value, bool is_group);
|
||||
|
||||
/* Parse --chown=USER:GROUP. Supports USER:GROUP, USER (owner only), :GROUP
|
||||
|
||||
+173
-1
@@ -10,6 +10,8 @@
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include "utils.h"
|
||||
#include <fnmatch.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -3537,6 +3539,172 @@ static void test_parse_args_usermap_rsync_forms() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Independent oracle for the FROM name-glob tests: enumerate the sender's
|
||||
* account database and fill `ids` with the DISTINCT ids whose name matches
|
||||
* `glob`, sorted ascending. Returns the count (bounded by `max`). */
|
||||
static int cli_collect_glob_ids(const char* glob, bool is_group, int32_t* ids, int max) {
|
||||
int n = 0;
|
||||
if (is_group) {
|
||||
setgrent();
|
||||
struct group* gr;
|
||||
while ((gr = getgrent()) != NULL) {
|
||||
if (fnmatch(glob, gr->gr_name, 0) != 0)
|
||||
continue;
|
||||
if ((unsigned long)gr->gr_gid > (unsigned long)INT32_MAX)
|
||||
continue;
|
||||
int32_t id = (int32_t)gr->gr_gid;
|
||||
bool dup = false;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (ids[i] == id)
|
||||
dup = true;
|
||||
if (!dup && n < max)
|
||||
ids[n++] = id;
|
||||
}
|
||||
endgrent();
|
||||
} else {
|
||||
setpwent();
|
||||
struct passwd* pw;
|
||||
while ((pw = getpwent()) != NULL) {
|
||||
if (fnmatch(glob, pw->pw_name, 0) != 0)
|
||||
continue;
|
||||
if ((unsigned long)pw->pw_uid > (unsigned long)INT32_MAX)
|
||||
continue;
|
||||
int32_t id = (int32_t)pw->pw_uid;
|
||||
bool dup = false;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (ids[i] == id)
|
||||
dup = true;
|
||||
if (!dup && n < max)
|
||||
ids[n++] = id;
|
||||
}
|
||||
endpwent();
|
||||
}
|
||||
for (int i = 1; i < n; i++) {
|
||||
int32_t key = ids[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && ids[j] > key) {
|
||||
ids[j + 1] = ids[j];
|
||||
j--;
|
||||
}
|
||||
ids[j + 1] = key;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static int cli_count_runs(const int32_t* ids, int n) {
|
||||
int runs = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i == 0 || ids[i - 1] == INT32_MAX || ids[i] != ids[i - 1] + 1)
|
||||
runs++;
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
/* #294: a FROM name wildcard must expand, at CLI-parse time, against the
|
||||
* sender's account database into numeric id/range rules. Prefer a prefix that
|
||||
* matches >=2 DISTINCT NON-contiguous ids (exercising multi-rule expansion); if
|
||||
* no such prefix exists on this host, fall back to one whose ids are contiguous
|
||||
* (exercising range collapse). The expected rules are derived independently by
|
||||
* enumerating the same database. */
|
||||
static void test_parse_args_identity_map_from_name_glob(bool is_group) {
|
||||
int32_t ids[512];
|
||||
int chosen_n = 0;
|
||||
int chosen_runs = 0;
|
||||
char chosen_c = 0;
|
||||
for (char c = 'a'; c <= 'z'; c++) {
|
||||
const char glob[3] = {c, '*', '\0'};
|
||||
int n = cli_collect_glob_ids(glob, is_group, ids, (int)(sizeof(ids) / sizeof(ids[0])));
|
||||
if (n < 2)
|
||||
continue;
|
||||
int runs = cli_count_runs(ids, n);
|
||||
if (runs >= 2 || chosen_c == 0) {
|
||||
chosen_c = c;
|
||||
chosen_n = n;
|
||||
chosen_runs = runs;
|
||||
}
|
||||
if (runs >= 2)
|
||||
break;
|
||||
}
|
||||
if (chosen_c == 0)
|
||||
return; /* no multi-match prefix on this host (skipped, not failed) */
|
||||
|
||||
const char glob[3] = {chosen_c, '*', '\0'};
|
||||
chosen_n = cli_collect_glob_ids(glob, is_group, ids, (int)(sizeof(ids) / sizeof(ids[0])));
|
||||
chosen_runs = cli_count_runs(ids, chosen_n);
|
||||
EXPECT_TRUE(chosen_n >= 2);
|
||||
|
||||
char map_value[16];
|
||||
snprintf(map_value, sizeof(map_value), "%s:@0", glob);
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", is_group ? "--groupmap" : "--usermap", map_value, "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
|
||||
int got = is_group ? cfg->groupmap_count : cfg->usermap_count;
|
||||
EXPECT_EQ_INT(got, chosen_runs);
|
||||
const IdentityMap* map = is_group ? cfg->groupmap : cfg->usermap;
|
||||
/* Every matched id is covered by some expanded rule. */
|
||||
for (int i = 0; i < chosen_n; i++) {
|
||||
bool covered = false;
|
||||
for (int r = 0; r < got; r++)
|
||||
if (ids[i] >= map[r].from && ids[i] <= map[r].from_hi)
|
||||
covered = true;
|
||||
EXPECT_TRUE(covered);
|
||||
}
|
||||
/* Every id inside every expanded range is one the glob actually matched, so
|
||||
* the range collapse cannot over-match a name that does not fit the glob. */
|
||||
for (int r = 0; r < got; r++) {
|
||||
EXPECT_EQ_INT(map[r].to, 0);
|
||||
for (int32_t v = map[r].from; v <= map[r].from_hi; v++) {
|
||||
bool expected = false;
|
||||
for (int i = 0; i < chosen_n; i++)
|
||||
if (ids[i] == v)
|
||||
expected = true;
|
||||
EXPECT_TRUE(expected);
|
||||
if (v == INT32_MAX)
|
||||
break;
|
||||
}
|
||||
}
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_parse_args_usermap_from_name_glob() {
|
||||
test_parse_args_identity_map_from_name_glob(false);
|
||||
}
|
||||
|
||||
static void test_parse_args_groupmap_from_name_glob() {
|
||||
test_parse_args_identity_map_from_name_glob(true);
|
||||
}
|
||||
|
||||
/* #294: an expansion that would push the map past MAX_IDENTITY_MAP must fail
|
||||
* with a clear error rather than silently truncating. Prefill the map to the
|
||||
* cap and then add a wildcard guaranteed to match at least the current user. */
|
||||
static void test_parse_args_identity_map_from_name_glob_over_cap() {
|
||||
const struct passwd* self = getpwuid(geteuid());
|
||||
if (!self || self->pw_name[0] == '\0')
|
||||
return;
|
||||
char glob[8];
|
||||
snprintf(glob, sizeof(glob), "%c*", self->pw_name[0]);
|
||||
|
||||
size_t need = (size_t)MAX_IDENTITY_MAP * 6 + strlen(glob) + 4 + 1;
|
||||
char* value = malloc(need);
|
||||
if (!value)
|
||||
return;
|
||||
size_t off = 0;
|
||||
for (int i = 0; i < MAX_IDENTITY_MAP; i++)
|
||||
off += (size_t)snprintf(value + off, need - off, "@0:@0,");
|
||||
snprintf(value + off, need - off, "%s:@0", glob);
|
||||
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--usermap", value, "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
free(value);
|
||||
}
|
||||
|
||||
/* #294: rsync refuses to mix --chown with --usermap/--groupmap on the same
|
||||
* side (either order). --chown=USER conflicts with a prior --usermap;
|
||||
* --chown=:GROUP conflicts with a prior --groupmap; the opposite side is fine. */
|
||||
@@ -3716,7 +3884,8 @@ static void test_parse_args_rejects_malformed_identity() {
|
||||
{"--usermap", "definitely_not_a_real_user_zzz:@1"},
|
||||
{"--usermap", "0-"},
|
||||
{"--usermap", "5-2:@1"},
|
||||
{"--usermap", "roo*:@1"},
|
||||
{"--usermap", "zzz_definitely_no_such_user_glob_zzz*:@1"},
|
||||
{"--groupmap", "zzz_definitely_no_such_group_glob_zzz*:@1"},
|
||||
{"--groupmap", "@1"},
|
||||
{"--groupmap", "no_such_group_qqq:x"},
|
||||
{"--chown", "a:b:c"},
|
||||
@@ -4882,6 +5051,9 @@ void test_client_cli() {
|
||||
test_parse_args_groupmap();
|
||||
test_parse_args_usermap_name_resolution();
|
||||
test_parse_args_usermap_rsync_forms();
|
||||
test_parse_args_usermap_from_name_glob();
|
||||
test_parse_args_groupmap_from_name_glob();
|
||||
test_parse_args_identity_map_from_name_glob_over_cap();
|
||||
test_parse_args_identity_map_chown_conflict();
|
||||
test_parse_args_chown();
|
||||
test_parse_args_copy_as();
|
||||
|
||||
+181
-1
@@ -3,11 +3,13 @@
|
||||
#include "test_utils.h"
|
||||
#include "transport_tcp.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <dirent.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* -4/-6 map to a getaddrinfo ai_family hint: -4 -> AF_INET, -6 -> AF_INET6,
|
||||
* and neither -> AF_UNSPEC. Both flags together are rejected earlier (in
|
||||
@@ -246,6 +248,180 @@ static void test_tcp_nodelay_default_and_override() {
|
||||
server_delete(&s);
|
||||
}
|
||||
|
||||
/* Count the process's open descriptors via /proc/self/fd. The opendir
|
||||
* descriptor is itself counted and closed before returning, so repeated calls
|
||||
* are consistent and a before/after delta reflects only the code under test. */
|
||||
static int count_open_fds(void) {
|
||||
DIR* dir = opendir("/proc/self/fd");
|
||||
if (!dir)
|
||||
return -1;
|
||||
int count = 0;
|
||||
const struct dirent* ent;
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
||||
continue;
|
||||
count++;
|
||||
}
|
||||
closedir(dir);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Bind + listen on the SECOND address getaddrinfo returns for "localhost", so
|
||||
* the first candidate is connection-refused and the shared connect loop must
|
||||
* fall back to a later one. Returns the listener fd and its port, or -1 when
|
||||
* this host does not resolve localhost to at least two addresses (the test then
|
||||
* skips rather than claiming coverage it does not have). */
|
||||
static int bind_second_localhost_address(int* out_port) {
|
||||
struct addrinfo hints;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
struct addrinfo* res = NULL;
|
||||
if (getaddrinfo("localhost", "0", &hints, &res) != 0 || !res)
|
||||
return -1;
|
||||
const struct addrinfo* chosen = res->ai_next;
|
||||
if (!chosen) {
|
||||
freeaddrinfo(res);
|
||||
return -1;
|
||||
}
|
||||
int fd = socket(chosen->ai_family, chosen->ai_socktype, chosen->ai_protocol);
|
||||
if (fd < 0) {
|
||||
freeaddrinfo(res);
|
||||
return -1;
|
||||
}
|
||||
int opt = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
if (bind(fd, chosen->ai_addr, chosen->ai_addrlen) != 0 || listen(fd, 1) != 0) {
|
||||
close(fd);
|
||||
freeaddrinfo(res);
|
||||
return -1;
|
||||
}
|
||||
struct sockaddr_storage bound;
|
||||
socklen_t bound_len = sizeof(bound);
|
||||
if (getsockname(fd, (struct sockaddr*)&bound, &bound_len) != 0) {
|
||||
close(fd);
|
||||
freeaddrinfo(res);
|
||||
return -1;
|
||||
}
|
||||
if (bound.ss_family == AF_INET6)
|
||||
*out_port = ntohs(((struct sockaddr_in6*)&bound)->sin6_port);
|
||||
else
|
||||
*out_port = ntohs(((struct sockaddr_in*)&bound)->sin_port);
|
||||
freeaddrinfo(res);
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* #219 AC3: when the first getaddrinfo candidate is refused, the connect loop
|
||||
* must fall back to the next address and end with exactly ONE open descriptor
|
||||
* (proving the failed attempt's fd was closed before the retry). */
|
||||
static void test_tcp_connect_falls_back_to_next_address() {
|
||||
int port = 0;
|
||||
int listener = bind_second_localhost_address(&port);
|
||||
if (listener < 0)
|
||||
return; /* localhost is single-address on this host: cannot exercise fallback */
|
||||
int before = count_open_fds();
|
||||
Client* c = client_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
EXPECT_TRUE(client_connect(c, "localhost", port));
|
||||
EXPECT_TRUE(c->file_descriptor >= 0);
|
||||
if (before >= 0)
|
||||
EXPECT_EQ_INT(count_open_fds(), before + 1);
|
||||
client_disconnect(c);
|
||||
if (before >= 0)
|
||||
EXPECT_EQ_INT(count_open_fds(), before);
|
||||
client_delete(c);
|
||||
close(listener);
|
||||
}
|
||||
|
||||
/* #219 AC3: a connect that fails on every candidate leaves at most one
|
||||
* descriptor (the last failed attempt) and none after client_disconnect. */
|
||||
static void test_tcp_connect_failed_attempts_do_not_leak_fds() {
|
||||
/* Reserve an ephemeral port, then close it: connecting to it must fail. */
|
||||
int probe = socket(AF_INET, SOCK_STREAM, 0);
|
||||
EXPECT_TRUE(probe >= 0);
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0;
|
||||
EXPECT_EQ_INT(bind(probe, (struct sockaddr*)&addr, sizeof(addr)), 0);
|
||||
socklen_t addr_len = sizeof(addr);
|
||||
EXPECT_EQ_INT(getsockname(probe, (struct sockaddr*)&addr, &addr_len), 0);
|
||||
int port = ntohs(addr.sin_port);
|
||||
close(probe);
|
||||
|
||||
int before = count_open_fds();
|
||||
Client* c = client_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
EXPECT_FALSE(client_connect(c, "localhost", port));
|
||||
if (before >= 0)
|
||||
EXPECT_TRUE(count_open_fds() <= before + 1);
|
||||
client_disconnect(c);
|
||||
if (before >= 0)
|
||||
EXPECT_EQ_INT(count_open_fds(), before);
|
||||
client_delete(c);
|
||||
}
|
||||
|
||||
/* #219 AC3: the shared tcp_connect_socket_ex() (used by both the plain and TLS
|
||||
* entry points) must install the --contimeout as SO_RCVTIMEO/SO_SNDTIMEO before
|
||||
* connecting. Calling it directly lets us observe the pre-connect state (the
|
||||
* plain wrapper later overrides the receive timeout with the IO --timeout). */
|
||||
static void test_tcp_connect_socket_ex_applies_contimeout() {
|
||||
Server* s = server_create(0);
|
||||
EXPECT_NOT_NULL(s);
|
||||
EXPECT_EQ_INT(listen(s->file_descriptor, 1), 0);
|
||||
struct sockaddr_in bound;
|
||||
socklen_t bound_len = sizeof(bound);
|
||||
EXPECT_EQ_INT(getsockname(s->file_descriptor, (struct sockaddr*)&bound, &bound_len), 0);
|
||||
int port = ntohs(bound.sin_port);
|
||||
|
||||
tcp_set_timeouts(30, 7);
|
||||
Client* c = client_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
TcpConnectOptions opts;
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
EXPECT_TRUE(tcp_connect_socket_ex(c, "127.0.0.1", port, &opts));
|
||||
struct timeval tv;
|
||||
socklen_t tv_len = sizeof(tv);
|
||||
EXPECT_EQ_INT(getsockopt(c->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &tv, &tv_len), 0);
|
||||
EXPECT_EQ_INT((int)tv.tv_sec, 7);
|
||||
tv_len = sizeof(tv);
|
||||
EXPECT_EQ_INT(getsockopt(c->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &tv, &tv_len), 0);
|
||||
EXPECT_EQ_INT((int)tv.tv_sec, 7);
|
||||
client_disconnect(c);
|
||||
client_delete(c);
|
||||
tcp_set_timeouts(30, 10);
|
||||
server_delete(&s);
|
||||
}
|
||||
|
||||
/* The plain wrapper applies the post-connect IO --timeout, which supersedes the
|
||||
* contimeout installed during connect. */
|
||||
static void test_tcp_connect_post_timeout_applied() {
|
||||
Server* s = server_create(0);
|
||||
EXPECT_NOT_NULL(s);
|
||||
EXPECT_EQ_INT(listen(s->file_descriptor, 1), 0);
|
||||
struct sockaddr_in bound;
|
||||
socklen_t bound_len = sizeof(bound);
|
||||
EXPECT_EQ_INT(getsockname(s->file_descriptor, (struct sockaddr*)&bound, &bound_len), 0);
|
||||
int port = ntohs(bound.sin_port);
|
||||
|
||||
tcp_set_timeouts(5, 7);
|
||||
Client* c = client_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
EXPECT_TRUE(client_connect(c, "127.0.0.1", port));
|
||||
struct timeval tv;
|
||||
socklen_t tv_len = sizeof(tv);
|
||||
EXPECT_EQ_INT(getsockopt(c->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &tv, &tv_len), 0);
|
||||
EXPECT_EQ_INT((int)tv.tv_sec, 5);
|
||||
tv_len = sizeof(tv);
|
||||
EXPECT_EQ_INT(getsockopt(c->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &tv, &tv_len), 0);
|
||||
EXPECT_EQ_INT((int)tv.tv_sec, 5);
|
||||
client_disconnect(c);
|
||||
client_delete(c);
|
||||
tcp_set_timeouts(30, 10);
|
||||
server_delete(&s);
|
||||
}
|
||||
|
||||
void test_transport_tcp() {
|
||||
test_server_create_ephemeral();
|
||||
test_server_delete_null();
|
||||
@@ -263,4 +439,8 @@ void test_transport_tcp() {
|
||||
test_server_create_bind_address();
|
||||
test_server_create_bind_ipv6();
|
||||
test_tcp_nodelay_default_and_override();
|
||||
test_tcp_connect_falls_back_to_next_address();
|
||||
test_tcp_connect_failed_attempts_do_not_leak_fds();
|
||||
test_tcp_connect_socket_ex_applies_contimeout();
|
||||
test_tcp_connect_post_timeout_applied();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#include "test_utils.h"
|
||||
#include "transport_tcp.h"
|
||||
#include "transport_tls.h"
|
||||
#include <dirent.h>
|
||||
#include <netinet/in.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_tls_global_init() {
|
||||
@@ -71,9 +74,53 @@ static void test_server_create_tls_empty_certs() {
|
||||
EXPECT_NULL(s);
|
||||
}
|
||||
|
||||
/* Count the process's open descriptors via /proc/self/fd (see the TCP tests). */
|
||||
static int tls_count_open_fds(void) {
|
||||
DIR* dir = opendir("/proc/self/fd");
|
||||
if (!dir)
|
||||
return -1;
|
||||
int count = 0;
|
||||
const struct dirent* ent;
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
||||
continue;
|
||||
count++;
|
||||
}
|
||||
closedir(dir);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* #219 AC3: client_connect_tls_ex() reuses the shared tcp_connect_socket_ex()
|
||||
* for the TCP connect, and a later TLS-setup failure must release that
|
||||
* descriptor. Passing no CA path makes create_ssl_ctx() fail deterministically
|
||||
* AFTER a successful TCP connect, so the cleanup path is exercised without a
|
||||
* TLS handshake or a certificate. (The multi-address fallback itself is covered
|
||||
* by the shared tcp_connect_socket_ex() tests in test_transport_tcp.c, which the
|
||||
* TLS entry point calls.) */
|
||||
static void test_client_connect_tls_releases_fd_on_setup_failure() {
|
||||
Server* s = server_create(0);
|
||||
EXPECT_NOT_NULL(s);
|
||||
EXPECT_EQ_INT(listen(s->file_descriptor, 1), 0);
|
||||
struct sockaddr_in bound;
|
||||
socklen_t bound_len = sizeof(bound);
|
||||
EXPECT_EQ_INT(getsockname(s->file_descriptor, (struct sockaddr*)&bound, &bound_len), 0);
|
||||
int port = ntohs(bound.sin_port);
|
||||
|
||||
int before = tls_count_open_fds();
|
||||
Client* c = client_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
EXPECT_FALSE(client_connect_tls(c, "127.0.0.1", port, NULL, NULL, NULL));
|
||||
EXPECT_TRUE(c->file_descriptor == -1);
|
||||
if (before >= 0)
|
||||
EXPECT_EQ_INT(tls_count_open_fds(), before);
|
||||
client_delete(c);
|
||||
server_delete(&s);
|
||||
}
|
||||
|
||||
void test_transport_tls() {
|
||||
test_tls_global_init();
|
||||
test_server_create_tls_without_certs();
|
||||
test_client_connect_tls_fail();
|
||||
test_client_connect_tls_releases_fd_on_setup_failure();
|
||||
test_server_create_tls_empty_certs();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user