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.

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.

For clarity, let's change the `int is_safe` to `bool safe` in
`struct safe_directory_data`.

Signed-off-by: Christian Couder <christian.couder@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
seen
Christian Couder 2026-09-08 18:41:26 +02:00 committed by Junio C Hamano
parent 6aa4adae0e
commit e63bb82850
2 changed files with 138 additions and 50 deletions

138
setup.c
View File

@ -1338,67 +1338,105 @@ 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.
*/
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 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->safe,
allow_safe_dir, &cbdata);

return 0;
}
@ -1440,7 +1478,7 @@ static int ensure_valid_ownership(const char *gitfile,
git_protected_config(safe_directory_cb, &data);

free(data.path);
return data.is_safe;
return data.safe;
}

void die_upon_dubious_ownership(const char *gitfile, const char *worktree,

50
setup.h
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 */