feat(cli): accept --inc-recursive no-op and in-root absolute --temp-dir
This commit is contained in:
+84
-29
@@ -2,6 +2,7 @@
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <libgen.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -198,6 +199,56 @@ static FileSaveResult hardlink_sibling_absent_first(const char* destination_path
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* Resolve a user-supplied --temp-dir against the receive `root`.
|
||||
*
|
||||
* A relative, traversal-free name is joined below the root (the historical
|
||||
* behavior). An absolute path is canonicalized with realpath(3) and accepted
|
||||
* only when it lies inside the canonicalized receive root; this is the parity
|
||||
* win over rejecting every absolute path, without weakening the confinement
|
||||
* invariant: an absolute path that escapes the root (including one reached
|
||||
* through a symlinked component) is still refused. A `..` component in a
|
||||
* relative name is likewise refused. The root itself is treated as an
|
||||
* absolute path free of `..`; its realpath() resolves any symlinks so the
|
||||
* prefix comparison is against one canonical form.
|
||||
*
|
||||
* Logs a clear error on rejection (the scratch dir must stay confined) and
|
||||
* returns a newly allocated scratch path, or NULL on rejection/allocation
|
||||
* failure. */
|
||||
static char* file_save_resolve_temp_dir(const char* root, const char* temp_dir) {
|
||||
if (temp_dir[0] != '/') {
|
||||
if (has_path_traversal(temp_dir)) {
|
||||
log_message(
|
||||
LOG_LEVEL_ERROR,
|
||||
"receiver rejected --temp-dir '%s': a '..' component would escape the receive root",
|
||||
temp_dir);
|
||||
return NULL;
|
||||
}
|
||||
return path_cat(root, temp_dir);
|
||||
}
|
||||
char canonical_temp[PATH_MAX];
|
||||
char canonical_root[PATH_MAX];
|
||||
if (!realpath(temp_dir, canonical_temp)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"receiver rejected --temp-dir '%s': could not resolve the absolute path (%s)",
|
||||
temp_dir, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
if (!realpath(root, canonical_root)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"receiver rejected --temp-dir '%s': could not resolve the receive root (%s)",
|
||||
temp_dir, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
if (strcmp(canonical_root, "/") != 0 && !path_is_within_root(canonical_root, canonical_temp)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"receiver rejected --temp-dir '%s': an absolute temp dir must be inside the "
|
||||
"receive root '%s'",
|
||||
temp_dir, canonical_root);
|
||||
return NULL;
|
||||
}
|
||||
return str_dup(canonical_temp);
|
||||
}
|
||||
|
||||
/* Install a --hard-links/-H sibling: the destination entry is atomically
|
||||
replaced (temp + rename) with a hard link to the group's first member. The
|
||||
first member is guaranteed already installed at `hardlink_target` under the
|
||||
@@ -303,17 +354,13 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
free(destination_path);
|
||||
return absent_result;
|
||||
}
|
||||
/* Resolve a relative --temp-dir under the destination root, exactly as the
|
||||
* primary save path does; an absolute or `..`-escaping value is rejected. */
|
||||
/* Resolve the --temp-dir under the destination root, exactly as the primary
|
||||
* save path does: a relative dir joins below the root, an absolute dir is
|
||||
* accepted only when it canonicalizes inside the root, and any escaping value
|
||||
* is rejected. */
|
||||
char* resolved_temp = NULL;
|
||||
if (cfg->temp_dir) {
|
||||
if (cfg->temp_dir[0] == '/' || has_path_traversal(cfg->temp_dir)) {
|
||||
free(content);
|
||||
free(first_disk);
|
||||
free(destination_path);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
resolved_temp = path_cat(root_directory, cfg->temp_dir);
|
||||
resolved_temp = file_save_resolve_temp_dir(root_directory, cfg->temp_dir);
|
||||
if (!resolved_temp) {
|
||||
free(content);
|
||||
free(first_disk);
|
||||
@@ -896,17 +943,17 @@ static bool file_save_try_special_dispatch(const FileSavePlan* plan, bool* creat
|
||||
and disk paths. Returns false on an invalid/escaping option or an
|
||||
allocation failure (the caller routes to the cleanup epilogue). */
|
||||
static bool file_save_resolve_paths(FileSavePlan* plan) {
|
||||
/* These options arrive from the client. --backup-dir, --partial-dir and
|
||||
--temp-dir are names below the server root, never independent filesystem
|
||||
roots: an absolute or `..`-escaping value is rejected outright (rsync's
|
||||
daemon confines temp-dir to the module the same way). A relative temp dir
|
||||
is resolved under the receive root below; if that resolution still lands on
|
||||
a different filesystem than the destination the install falls back to a
|
||||
non-atomic copy (see file_to_disk_secure_impl), never an abort. */
|
||||
/* These options arrive from the client. --backup-dir and --partial-dir are
|
||||
names below the server root, never independent filesystem roots: an
|
||||
absolute or `..`-escaping value is rejected outright. --temp-dir is
|
||||
resolved by file_save_resolve_temp_dir below: a relative name joins below
|
||||
the root, an absolute name is accepted only when it canonicalizes inside
|
||||
the root, and any escaping value is rejected. If the resolved scratch dir
|
||||
still lands on a different filesystem than the destination the install
|
||||
falls back to a non-atomic copy (see file_to_disk_secure_impl), never an
|
||||
abort. */
|
||||
if ((plan->backup_dir && (plan->backup_dir[0] == '/' || has_path_traversal(plan->backup_dir))) ||
|
||||
(plan->partial_dir &&
|
||||
(plan->partial_dir[0] == '/' || has_path_traversal(plan->partial_dir))) ||
|
||||
(plan->temp_dir && (plan->temp_dir[0] == '/' || has_path_traversal(plan->temp_dir))))
|
||||
(plan->partial_dir && (plan->partial_dir[0] == '/' || has_path_traversal(plan->partial_dir))))
|
||||
return false;
|
||||
if (plan->backup_dir &&
|
||||
!(plan->confined_backup = path_cat(plan->root_directory, plan->backup_dir)))
|
||||
@@ -914,6 +961,9 @@ static bool file_save_resolve_paths(FileSavePlan* plan) {
|
||||
if (plan->partial_dir &&
|
||||
!(plan->confined_partial = path_cat(plan->root_directory, plan->partial_dir)))
|
||||
return false;
|
||||
if (plan->temp_dir &&
|
||||
!(plan->confined_temp = file_save_resolve_temp_dir(plan->root_directory, plan->temp_dir)))
|
||||
return false;
|
||||
|
||||
const char* actual_root = plan->use_partial_root ? plan->confined_partial : plan->root_directory;
|
||||
plan->destination_path = path_cat(plan->root_directory, plan->file->path);
|
||||
@@ -1146,17 +1196,22 @@ FileSaveResult file_save_to_disk_full_ex(const char* root_directory, const File*
|
||||
|
||||
/* A configured --temp-dir sends the temporary working copy to a scratch
|
||||
directory; the engine then atomically renames the completed file into the
|
||||
final destination directory. A relative temp dir is resolved under the
|
||||
receive root and must already exist (an absolute or `..`-escaping value was
|
||||
rejected above); the engine falls back to a non-atomic copy on EXDEV. The
|
||||
partial-dir flow already keeps its working copy in a separate directory and
|
||||
--inplace writes directly, so neither diverts through the scratch dir
|
||||
(matching rsync, where --inplace/--partial-dir supersede --temp-dir). */
|
||||
bool use_temp_dir = plan.temp_dir != NULL && !plan.inplace && !plan.use_partial_root;
|
||||
final destination directory. The scratch path was confined to the receive
|
||||
root (and canonicalized) in file_save_resolve_paths and must already exist;
|
||||
the engine falls back to a non-atomic copy on EXDEV. The partial-dir flow
|
||||
already keeps its working copy in a separate directory and --inplace writes
|
||||
directly, so neither diverts through the scratch dir (matching rsync, where
|
||||
--inplace/--partial-dir supersede --temp-dir). */
|
||||
/* --inplace and --partial-dir supersede --temp-dir in rsync, so the scratch
|
||||
dir is not used on those paths. The value was still validated/confined by
|
||||
file_save_resolve_paths; drop the resolved path so it is never handed to the
|
||||
install engine. */
|
||||
if (plan.confined_temp && (plan.inplace || plan.use_partial_root)) {
|
||||
free(plan.confined_temp);
|
||||
plan.confined_temp = NULL;
|
||||
}
|
||||
bool use_temp_dir = plan.confined_temp != NULL;
|
||||
if (use_temp_dir) {
|
||||
plan.confined_temp = path_cat(root_directory, plan.temp_dir);
|
||||
if (!plan.confined_temp)
|
||||
goto out;
|
||||
/* A user-supplied trailing slash would leave the scratch path ending in
|
||||
"/", which has no final component to create/open. Normalize it away. */
|
||||
size_t temp_len = strlen(plan.confined_temp);
|
||||
|
||||
Reference in New Issue
Block a user