fix(filter): correct per-directory rule owner coordinate; post-clear ownership and bounds

This commit is contained in:
2026-09-24 00:24:52 +02:00
parent b414f197af
commit 884530c9a2
9 changed files with 351 additions and 49 deletions
+23 -2
View File
@@ -334,7 +334,10 @@ static const char* filter_dir_rule_owner(const FilterRule* rule) {
* order = the sender's traversal/rule order). Rules of one directory are
* appended to the sink contiguously, so runs reproduce the compilation order.
* Bounded by MAX_FILTER_RULES / MAX_FILTER_BYTES and MAX_PROTECT_PATTERN_LEN so
* the peer never sees a frame it would reject. */
* the peer never sees a frame it would reject. The sender enforces exactly the
* receiver's limits (including the cumulative owner+pattern byte budget) and
* fails with a clear local error instead of emitting a frame that would abort
* the transfer with STATUS_ERROR. */
bool delete_filter_dir_rules_send(int fd, const FilterRuleList* rules) {
int count = rules ? rules->count : 0;
if (count < 0 || count > MAX_FILTER_RULES) {
@@ -342,13 +345,31 @@ bool delete_filter_dir_rules_send(int fd, const FilterRuleList* rules) {
MAX_FILTER_RULES);
return false;
}
size_t bytes = 0;
for (int i = 0; i < count; i++) {
const FilterRule* rule = rules->items[i];
const char* owner = filter_dir_rule_owner(rule);
size_t owner_len = strlen(owner);
size_t pattern_len = rule && rule->pattern ? strlen(rule->pattern) : 0;
if (!rule || !rule->pattern || pattern_len == 0 || pattern_len > MAX_PROTECT_PATTERN_LEN) {
if (!rule || !rule->pattern || pattern_len == 0) {
log_message(LOG_LEVEL_ERROR, "invalid per-directory filter pattern");
return false;
}
if (pattern_len > MAX_PROTECT_PATTERN_LEN) {
log_message(LOG_LEVEL_ERROR,
"per-directory filter pattern exceeds %d bytes (use a shorter pattern)",
MAX_PROTECT_PATTERN_LEN);
return false;
}
if (!(owner_len == 0 || (owner[0] != '/' && !has_path_traversal(owner)))) {
log_message(LOG_LEVEL_ERROR, "invalid per-directory filter owner directory");
return false;
}
if (owner_len + pattern_len > MAX_FILTER_BYTES - bytes) {
log_message(LOG_LEVEL_ERROR, "per-directory filter rules exceed %d bytes", MAX_FILTER_BYTES);
return false;
}
bytes += owner_len + pattern_len;
}
int groups = 0;
for (int i = 0; i < count;) {
+55 -27
View File
@@ -11,8 +11,6 @@
/* Write a diagnostic message into the caller's optional buffer. */
#define filter_set_error utils_set_error
static bool set_rule_owner(FilterRule* rule, const char* owner);
/* ---- Ordered rule lists ---- */
void filter_rule_free(FilterRule* rule) {
@@ -93,7 +91,7 @@ static bool filter_list_add_exclude_self(FilterRuleList* list, const char* name)
rule->action = FILTER_ACTION_EXCLUDE;
rule->sides = FILTER_SIDE_SENDER | FILTER_SIDE_RECEIVER;
rule->pattern = str_dup(base);
if (!rule->pattern || !set_rule_owner(rule, "")) {
if (!rule->pattern || !filter_rule_set_owner(rule, "")) {
filter_rule_free(rule);
return false;
}
@@ -116,10 +114,22 @@ bool filter_rule_list_add_dir_merge_ex(FilterRuleList* list, const char* name, b
if (!list || !name || name[0] == '\0')
return false;
for (int i = 0; i < list->dir_merge_count; i++) {
if (strcmp(list->dir_merges[i].name, name) == 0)
if (strcmp(list->dir_merges[i].name, name) == 0) {
/* rsync keeps the first registration (first-wins), but the 'e' modifier
is a list side effect, not a registration field: honor it on the
duplicate path too, adding the implicit exclude-self rule at most
once. */
if (exclude_self && !list->dir_merges[i].exclude_self) {
if (!filter_list_add_exclude_self(list, name))
return false;
list->dir_merges[i].exclude_self = true;
}
return true;
}
}
if (list->dir_merge_count == list->dir_merge_capacity) {
if (list->dir_merge_capacity > INT_MAX / 2)
return false;
int new_cap = list->dir_merge_capacity > 0 ? list->dir_merge_capacity * 2 : 4;
FilterDirMerge* grown = realloc(list->dir_merges, (size_t)new_cap * sizeof(*grown));
if (!grown)
@@ -145,7 +155,9 @@ bool filter_rule_list_add_dir_merge_ex(FilterRuleList* list, const char* name, b
return true;
}
static bool set_rule_owner(FilterRule* rule, const char* owner) {
bool filter_rule_set_owner(FilterRule* rule, const char* owner) {
if (!rule)
return false;
char* dup = str_dup(owner ? owner : "");
if (!dup)
return false;
@@ -596,7 +608,7 @@ static bool filter_list_append_cvs(FilterRuleList* list, unsigned sides) {
}
memcpy(rule->pattern, CVS_DEFAULTS[i].pattern, plen);
rule->pattern[plen] = '\0';
if (!set_rule_owner(rule, "")) {
if (!filter_rule_set_owner(rule, "")) {
filter_rule_free(rule);
return false;
}
@@ -647,7 +659,12 @@ static bool filter_merge_read(FilterRuleList* list, FILE* fp, const char* displa
const FilterDirMerge* spec, const FilterParseOptions* opts,
const char* base_dir, const char* owner_rel, int depth, char* err,
size_t err_size) {
int rules_before = list->count;
/* Lowest list index this read is responsible for. A "clear"/"!" inside the
* file resets list->count to 0 (freeing the caller's earlier rules too), so
* the base must follow it down: otherwise post-clear rules sit below the
* original count and never receive an owner (nor no-inherit) and are missed
* by the rollback. */
int floor = list->count;
char* line = NULL;
size_t cap = 0;
bool ok = true;
@@ -687,6 +704,8 @@ static bool filter_merge_read(FilterRuleList* list, FILE* fp, const char* displa
token[tlen] = '\0';
if (!filter_merge_append_token(list, token, spec, opts, base_dir, depth, err, err_size))
ok = false;
if (list->count < floor)
floor = list->count; /* a "clear" reset the list below this read's base */
free(token);
}
} else {
@@ -697,27 +716,28 @@ static bool filter_merge_read(FilterRuleList* list, FILE* fp, const char* displa
continue;
if (!filter_merge_append_token(list, lp, spec, opts, base_dir, depth, err, err_size))
ok = false;
if (list->count < floor)
floor = list->count; /* a "clear" reset the list below this read's base */
}
}
free(line);
if (!ok) {
/* Drop the rules this read appended (a "clear" inside the file may have
* freed earlier rules too; clamp like filter_file_rollback). */
int first = rules_before < list->count ? rules_before : list->count;
for (int i = first; i < list->count; i++)
/* Drop every live rule this read is responsible for. After a "clear" that
* base is 0, so the post-clear rules are freed too instead of leaking. */
for (int i = floor; i < list->count; i++)
filter_rule_free(list->items[i]);
list->count = first;
list->count = floor;
return false;
}
for (int i = rules_before; i < list->count; i++) {
for (int i = floor; i < list->count; i++) {
FilterRule* rule = list->items[i];
if (spec->no_inherit)
rule->no_inherit = true;
if (owner_rel && !set_rule_owner(rule, owner_rel)) {
if (owner_rel && !filter_rule_set_owner(rule, owner_rel)) {
filter_set_error(err, err_size, "memory allocation failed");
for (int j = rules_before; j < list->count; j++)
for (int j = floor; j < list->count; j++)
filter_rule_free(list->items[j]);
list->count = rules_before;
list->count = floor;
return false;
}
}
@@ -912,11 +932,10 @@ FilterRuleList* filter_base_build(const char* const* rule_texts, int rule_count,
/* Undo the rules and dir-merge registrations that one merge file appended,
* leaving the caller's earlier content intact. A "clear" rule inside the file
* frees every rule, including the caller's; clamp to the surviving count so
* those already-freed rules are never resurrected and freed a second time. */
/* Undo the rules and dir-merge registrations that one merge file appended,
* leaving the caller's earlier content intact. A "clear" rule inside the file
* frees every rule, including the caller's; clamp to the surviving count so
* those already-freed rules are never resurrected and freed a second time. */
* those already-freed rules are never resurrected and freed a second time.
* (filter_merge_read() has already rolled its own range back by the time this
* runs, so on a post-clear failure `list->count` is below `rules_before` and
* this is a no-op for the rules.) */
static void filter_file_rollback(FilterRuleList* list, int rules_before, int dir_merges_before) {
int first = rules_before < list->count ? rules_before : list->count;
for (int i = first; i < list->count; i++)
@@ -1016,11 +1035,16 @@ static FilterAction rule_matches(const FilterRule* rule, const char* rel_path, c
return FILTER_ACTION_NONE;
if (!(rule->sides & side))
return FILTER_ACTION_NONE;
/* A rule applies only to entries below its owner directory. */
/* A rule applies only to entries below its owner directory. The receive
* root's destination-relative coordinate may be written as "." (the
* synced-directory sentinel), which is the same scope as the empty owner. */
const char* owner = rule->owner;
if (owner && strcmp(owner, ".") == 0)
owner = "";
const char* rel2 = rel_path;
if (rule->owner && rule->owner[0] != '\0') {
size_t owner_len = strlen(rule->owner);
if (strncmp(rule->owner, rel_path, owner_len) != 0)
if (owner && owner[0] != '\0') {
size_t owner_len = strlen(owner);
if (strncmp(owner, rel_path, owner_len) != 0)
return FILTER_ACTION_NONE;
if (rel_path[owner_len] != '/')
return FILTER_ACTION_NONE;
@@ -1078,10 +1102,14 @@ FilterAction filter_dir_rules_apply_side(const FilterRuleList* dir_rules, const
for (;;) {
for (int i = 0; i < dir_rules->count; i++) {
const FilterRule* rule = dir_rules->items[i];
size_t rule_owner_len = rule && rule->owner ? strlen(rule->owner) : 0;
const char* rule_owner = rule && rule->owner ? rule->owner : "";
/* "." is the receive root's coordinate (see rule_matches). */
if (strcmp(rule_owner, ".") == 0)
rule_owner = "";
size_t rule_owner_len = strlen(rule_owner);
if (rule_owner_len != owner_len)
continue;
if (owner_len != 0 && memcmp(rule->owner, rel_path, owner_len) != 0)
if (owner_len != 0 && memcmp(rule_owner, rel_path, owner_len) != 0)
continue;
FilterAction action = rule_matches(rule, rel_path, leaf, is_dir, FILTER_SIDE_RECEIVER);
if (action != FILTER_ACTION_NONE)
+5
View File
@@ -100,6 +100,11 @@ FilterRule* filter_rule_parse(const char* line, const FilterParseOptions* opts,
void filter_rule_free(FilterRule* rule);
/* Deep-copy a rule (owned pattern/owner). Returns NULL on allocation failure. */
FilterRule* filter_rule_clone(const FilterRule* rule);
/* Replace a rule's owner directory (owned copy of `owner`, "" for the transfer
* root). Returns false on allocation failure, leaving the rule unchanged.
* Used to re-express a mirrored per-directory rule in the receiver's
* destination-relative coordinate system. */
bool filter_rule_set_owner(FilterRule* rule, const char* owner);
FilterRuleList* filter_rule_list_create(void);
/* Append a fully-parsed rule (takes ownership). Returns false on OOM. */