Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/libcrun/cgroup.c
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ libcrun_cgroup_enter_finalize (struct libcrun_cgroup_args *args, struct libcrun_
if (delegate_cgroup == NULL)
return 0;

if (path_has_dot_dot_component (delegate_cgroup))
return crun_make_error (err, 0, "invalid `..` component in delegate-cgroup `%s`", delegate_cgroup);

cgroup_mode = libcrun_get_cgroup_mode (err);
if (UNLIKELY (cgroup_mode < 0))
return cgroup_mode;
Expand Down
25 changes: 22 additions & 3 deletions src/libcrun/status.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,31 @@ struct pid_stat
unsigned long long starttime;
};

/* If ID is not NULL, then ennsure that it does not contain any slash. */
static inline bool
is_valid_id_char (char c, bool first)
{
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
return true;
if (first)
return false;
return c == '_' || c == '.' || c == '-';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Podman use this regex: ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$.

Docker use this regex: ^[a-zA-Z0-9][a-zA-Z0-9_.-]+$. The difference is, one-letter names are not allowed (I checked that).

Both Docker and Podman are slightly more restrictve than runc:

  1. runc allows all this and also '+' (since ~2016, Allow + in container ID opencontainers/runc#675); not sure why.
  2. runc allows _.- as the first character (but disallows names like '.' and '..').

NOTE Docker/Podman container names are not the same as crun/runc container IDs. Both Docker/containerd and Podman (I guess k8s, too) generate a random 64 character hexadecimal ID (like 0dda2d02664f88b49fae96705383da6da90149a49476ff6b0ee74b2ae3936c2f) which is used by crun/runc.

I'm not quite sure but maybe it makes sense to stay compatible with runc here (i.e. be less restrictive than docker/podman). The runc validation logic is here: https://github.com/opencontainers/runc/blob/main/libcontainer/factory_linux.go#L192

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runc convention makes the validation a bit more difficult as we need to treat the string as a path but doable. In any case it is a best effort measure, I will change the implementation to match runc

}
Comment thread
giuseppe marked this conversation as resolved.

/* Ensure the ID is not empty and matches [a-zA-Z0-9][a-zA-Z0-9_.-]*. */
static int
validate_id (const char *id, libcrun_error_t *err)
{
if (id && strchr (id, '/') != NULL)
return crun_make_error (err, 0, "invalid character `/` in the ID `%s`", id);
if (id == NULL)
return 0;

if (id[0] == '\0')
return crun_make_error (err, 0, "empty container ID");

for (size_t i = 0; id[i]; i++)
{
if (! is_valid_id_char (id[i], i == 0))
return crun_make_error (err, 0, "invalid character `%c` in the container ID `%s`", id[i], id);
}

return 0;
}
Expand Down
28 changes: 28 additions & 0 deletions src/libcrun/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,34 @@ is_empty_string (const char *s)
return s == NULL || s[0] == '\0';
}

static inline bool
path_has_dot_dot_component (const char *path)
{
if (path == NULL)
return false;
cleanup_free char *npath = xstrdup (path);
Comment thread
giuseppe marked this conversation as resolved.
char *cur, *it;

cur = npath;
it = strchr (cur, '/');
while (cur)
{
if (it)
*it = '\0';

if (strcmp (cur, "..") == 0)
return true;

if (it == NULL)
break;

cur = consume_slashes (it + 1);
*it = '/';
it = strchr (cur, '/');
}
return false;
}

static inline int
waitpid_ignore_stopped (pid_t pid, int *status, int options)
{
Expand Down
34 changes: 34 additions & 0 deletions tests/test_cgroup_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,39 @@ def test_annotation_delegate_cgroup():
run_crun_command(["delete", "-f", cid])


def test_annotation_delegate_cgroup_dotdot():
"""Test that run.oci.delegate-cgroup rejects '..' components."""
if not is_cgroup_v2_unified():
return (77, "requires cgroup v2")
if not running_on_systemd():
return (77, "requires systemd")
if get_cgroup_manager() != 'systemd':
return (77, "requires systemd cgroup manager")

conf = base_config()
add_all_namespaces(conf, cgroupns=True)
conf['process']['args'] = ['/init', 'echo', 'hello']

if 'annotations' not in conf:
conf['annotations'] = {}
conf['annotations']['run.oci.systemd.subgroup'] = 'mysubgroup'

for invalid in ["../../../victim", "../escape", "a/../b"]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would add ".." to the list.

conf['annotations']['run.oci.delegate-cgroup'] = invalid
try:
out, _ = run_and_get_output(conf, hide_stderr=True)
return -1
except Exception as e:
if hasattr(e, 'output') and e.output:
err = e.output.decode()
if "delegate-cgroup" in err:
continue
logger.info("got error for delegate-cgroup '%s': %s", invalid, err)
else:
logger.info("got exception without output for delegate-cgroup '%s': %s", invalid, str(e))
return -1
return 0

def test_annotation_systemd_force_cgroup_v1():
"""Test run.oci.systemd.force_cgroup_v1 annotation."""
if not is_cgroup_v2_unified():
Expand Down Expand Up @@ -1544,6 +1577,7 @@ def test_cgroup_subcgroup_cleanup():
"cgroup-create-without-resources": test_cgroup_create_without_resources,
"annotation-systemd-subgroup": test_annotation_systemd_subgroup,
"annotation-delegate-cgroup": test_annotation_delegate_cgroup,
"annotation-delegate-cgroup-dotdot": test_annotation_delegate_cgroup_dotdot,
"annotation-systemd-force-cgroup-v1": test_annotation_systemd_force_cgroup_v1,
"cgroup-v2-mount-options": test_cgroup_v2_mount_options,
"cgroup-subcgroup-cleanup": test_cgroup_subcgroup_cleanup,
Expand Down
23 changes: 22 additions & 1 deletion tests/test_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,14 +523,34 @@ def test_invalid_id():
except Exception as e:
if hasattr(e, 'output') and e.output:
err = e.output.decode()
if "invalid character `/` in the ID" in err:
if "invalid character" in err and "container ID" in err:
return 0
logger.info("got error: %s", err)
else:
logger.info("got exception without output: %s", str(e))
return -1
return 0

def test_invalid_id_chars():
conf = base_config()
conf['process']['args'] = ['./init', 'echo', 'hello']
conf['process']['cwd'] = "/sbin"
add_all_namespaces(conf)
for invalid_id in [".", "..", ".foo", "_foo", "foo bar", "foo/bar"]:
try:
out, _ = run_and_get_output(conf, id_container=invalid_id)
return -1
except Exception as e:
if hasattr(e, 'output') and e.output:
err = e.output.decode()
if "invalid" in err.lower():
continue
logger.info("got error for id '%s': %s", invalid_id, err)
else:
logger.info("got exception without output for id '%s': %s", invalid_id, str(e))
return -1
return 0

def test_home_unknown_id():
if is_rootless():
return (77, "requires root privileges")
Expand Down Expand Up @@ -610,6 +630,7 @@ def test_systemd_cgroups_path_def_slice():
"ioprio": test_ioprio,
"run-keep": test_run_keep,
"invalid-id": test_invalid_id,
"invalid-id-chars": test_invalid_id_chars,
"home-unknown-id": test_home_unknown_id,
"help": test_start_help,
"systemd-cgroups-path-def-slice": test_systemd_cgroups_path_def_slice,
Expand Down
Loading