setup: extract path_allowlist_apply()

In a following commit we are going to check whether a repository is
part of an allowlist specified in a config variable.

To prepare for that let's extract existing code from
safe_directory_cb() into a new path_allowlist_apply() helper that will
help with such checks.

While at it let's make the helper's code simpler and more generic, by
passing it a `bool (*allow_path)(const char *path, void *cbdata)`
function that decides if a path is acceptable by the caller, and let's
add a NEEDSWORK comment about it silently ignoring missing, possibly
misspelled, paths.

To further simplify how to reuse that new helper, and avoid duplicating
the config-value handling in a future commit, let's also introduce a
path_allowlist_config_apply() helper.

As the new path_allowlist_apply() function reports its result through
a `bool *matches` argument, let's also change the `int is_safe` member
of `struct safe_directory_data` to a `bool`, so that its address can
be passed as that argument.

Signed-off-by: Christian Couder <christian.couder@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Christian Couder authored and Junio C Hamano committed 2026-10-02 07:40:34 -07:00
1 parent 010fd39c3e
commit 96715284f4
2 files changed
+143 -49

No files matched your search

+93 -49
View File
@@ -1338,67 +1338,111 @@ static int canonicalize_ceiling_entry(struct string_list_item *item,
}
}
void path_allowlist_apply(const char *allowed, const char *target_path,
bool *matches,
bool (*allow_path)(const char *path, void *cbdata),
void *allow_path_cbdata)
{
char *normalized = NULL;
if (!allowed || !*allowed) {
*matches = false;
return;
}
if (!strcmp(allowed, "*")) {
*matches = true;
return;
}
if (!allow_path(allowed, allow_path_cbdata))
return;
/*
* A .gitconfig in $HOME may be shared across different
* machines and the config variable entries may or may not
* exist as paths on all of these machines. In other words,
* it is not a warning worthy event when there is no such path
* on this machine---the entry may be useful elsewhere.
*
* NEEDSWORK: this also silently ignores misspelled paths. We
* may want to warn about a missing path unless it is marked
* as allowed to be missing, e.g., with an ":(optional)"
* prefix like pathname-typed configuration values, and hint
* about that prefix in the warning.
*/
normalized = real_pathdup(allowed, 0);
if (!normalized)
return;
if (ends_with(normalized, "/*")) {
size_t len = strlen(normalized);
if (!fspathncmp(normalized, target_path, len - 1))
*matches = true;
} else if (!fspathcmp(target_path, normalized)) {
*matches = true;
}
free(normalized);
}
void path_allowlist_config_apply(const char *key, const char *value,
const char *target_path, bool *matches,
bool (*allow_path)(const char *path, void *cbdata),
void *allow_path_cbdata)
{
char *allowed = NULL;
if (!value || !*value || !strcmp(value, "*")) {
path_allowlist_apply(value, target_path, matches,
allow_path, allow_path_cbdata);
return;
}
if (git_config_pathname(&allowed, key, value) || !allowed)
return;
path_allowlist_apply(allowed, target_path, matches,
allow_path, allow_path_cbdata);
free(allowed);
}
/*
* Setting the config variable to a non-absolute path makes
* little sense---it won't be relative to the configuration
* file the item is defined in. Except for ".", which means
* "if we are at the top level of a repository, then it is
* OK", which is slightly tighter than "*" that allows
* discovery.
*/
static bool allow_safe_dir(const char *path, void *cbdata_)
{
struct path_allowlist_cb_data *cbdata = cbdata_;
if (is_absolute_path(path) || !strcmp(path, "."))
return true;
warning(_("%s '%s' not absolute"), cbdata->key, path);
return false;
}
struct safe_directory_data {
char *path;
int is_safe;
bool is_safe;
};
static int safe_directory_cb(const char *key, const char *value,
const struct config_context *ctx UNUSED, void *d)
{
struct safe_directory_data *data = d;
struct path_allowlist_cb_data cbdata = { .key = key };
if (strcmp(key, "safe.directory"))
return 0;
if (!value || !*value) {
data->is_safe = 0;
} else if (!strcmp(value, "*")) {
data->is_safe = 1;
} else {
char *allowed = NULL;
if (!git_config_pathname(&allowed, key, value) && allowed) {
char *normalized = NULL;
/*
* Setting safe.directory to a non-absolute path
* makes little sense---it won't be relative to
* the configuration file the item is defined in.
* Except for ".", which means "if we are at the top
* level of a repository, then it is OK", which is
* slightly tighter than "*" that allows discovery.
*/
if (!is_absolute_path(allowed) && strcmp(allowed, ".")) {
warning(_("safe.directory '%s' not absolute"),
allowed);
goto next;
}
/*
* A .gitconfig in $HOME may be shared across
* different machines and safe.directory entries
* may or may not exist as paths on all of these
* machines. In other words, it is not a warning
* worthy event when there is no such path on this
* machine---the entry may be useful elsewhere.
*/
normalized = real_pathdup(allowed, 0);
if (!normalized)
goto next;
if (ends_with(normalized, "/*")) {
size_t len = strlen(normalized);
if (!fspathncmp(normalized, data->path, len - 1))
data->is_safe = 1;
} else if (!fspathcmp(data->path, normalized)) {
data->is_safe = 1;
}
next:
free(normalized);
free(allowed);
}
}
path_allowlist_config_apply(key, value, data->path, &data->is_safe,
allow_safe_dir, &cbdata);
return 0;
}
+50
View File
@@ -304,4 +304,54 @@ struct startup_info {
extern struct startup_info *startup_info;
extern const char *tmp_original_cwd;
/* Path allowlist */
struct path_allowlist_cb_data {
const char *key;
};
/*
* Check the allowlist entry in `allowed` against `target_path`,
* updating `*matches` accordingly.
*
* `allowed` is a single entry of an allowlist of paths, typically one
* value of a multi-valued config variable, already expanded by
* git_config_pathname(). `target_path` is the (normalized) path being
* tested. `*matches` is updated in place:
*
* - an empty `allowed` resets it to 'false' (so a later, more
* specific config scope can clear entries from a broader one),
* - "*" sets it to 'true' (allow everything),
* - "<path>" sets it to 'true' if <path> equals `target_path`,
* - "<path>" + "/" + "*" sets it to 'true' if <path> is a leading
* directory of `target_path`,
* - anything else leaves `*matches` unchanged.
*
* `allow_path` is called with `allowed` and `allow_path_cbdata`, and
* should return 'true' if the entry is acceptable to the caller. It
* lets each caller decide which paths it is willing to consider, and
* whether to warn about the ones it rejects. Returning 'false' leaves
* `*matches` unchanged.
*
* Callers are expected to invoke this once per allowlist entry,
* typically from a protected-config callback, so that untrusted
* repository config cannot influence the decision.
*/
void path_allowlist_apply(const char *allowed, const char *target_path,
bool *matches,
bool (*allow_path)(const char *path, void *cbdata),
void *allow_path_cbdata);
/*
* Apply one value of a multi-valued config variable holding an
* allowlist of paths, expanding it with git_config_pathname() before
* checking it against `target_path`. Empty and "*" values are passed
* through without expansion, as interpolating them is not
* meaningful. See path_allowlist_apply().
*/
void path_allowlist_config_apply(const char *key, const char *value,
const char *target_path, bool *matches,
bool (*allow_path)(const char *path, void *cbdata),
void *allow_path_cbdata);
#endif /* SETUP_H */