Andrei reported an issue on arm64 where he was seeing the following
error message when running the tests
2024/01/17 18:32:31.109 [error] 54904#54904 "gidmap" field has an entry with "size": 1, but for unprivileged unit it must be 1.
This error message is guarded by the following if statement
if (nxt_slow_path(m.size > 1)
Turns out size was indeed > 1, in this case it was 289356276058554369,
m.size is defined as a nxt_int_t, which on arm64 is actually 8 bytes,
but was being printed as a signed int (4 bytes) and by chance/undefined
behaviour comes out as 1.
But why is size so big? In this case it should have just been 1 with a
config of
'gidmap': [{'container': 0, 'host': os.getegid(), 'size': 1}],
This is due to nxt_int_t being 64bits on arm64 but using a conf type of
NXT_CONF_MAP_INT which means in nxt_conf_map_object() we would do (using
our m.size variable as an example)
ptr = nxt_pointer_to(data, map[i].offset);
...
ptr->i = num;
Where ptr is a union pointer and is now pointing at our m.size
Next we set m.size to the value of num (which is 1 in this case), via
ptr->i where i is a member of that union of type int.
So here we are setting a 64bit memory location (nxt_int_t on arm64)
through a 32bit (int) union alias, this means we are only setting the
lower half (4) of the bytes.
Whatever happens to be in the upper 4 bytes will remain, giving us our
exceptionally large value.
This is demonstrated by this program
#include <stdio.h>
#include <stdint.h>
int main(void)
{
int64_t num = -1; /* All 1's in two's complement */
union {
int32_t i32;
int64_t i64;
} *ptr;
ptr = (void *)#
ptr->i32 = 1;
printf("num : %lu / %ld\n", num, num);
ptr->i64 = 1;
printf("num : %ld\n", num);
return 0;
}
$ make union-32-64-issue
cc union-32-64-issue.c -o union-32-64-issue
$ ./union-32-64-issue
num : 18446744069414584321 / -4294967295
num : 1
However that is not the only issue, because the members of
nxt_clone_map_entry_t were specified as nxt_int_t's on the likes of
x86_64 this would be a 32bit signed integer. However uid/gids on Linux
at least are defined as unsigned integers, so a nxt_int_t would not be
big enough to hold all potential values.
We could make the nxt_uint_t's but then we're back to the above union
aliasing problem.
We could just set the memory for these variables to 0 and that would
work, however that's really just papering over the problem.
The right thing is to use a large enough sized type to store these
things, hence the previously introduced nxt_cred_t. This is an int64_t
which is plenty large enough.
So we switch the nxt_clone_map_entry_t structure members over to
nxt_cred_t's and use NXT_CONF_MAP_INT64 as the conf type, which then
uses the right sized union member in nxt_conf_map_object() to set these
variables.
Reported-by: Andrei Zeliankou <zelenkov@nginx.com>
Reviewed-by: Zhidao Hong <z.hong@f5.com>
Signed-off-by: Andrew Clayton <a.clayton@nginx.com>
435 lines
11 KiB
C
435 lines
11 KiB
C
/*
|
|
* Copyright (C) Igor Sysoev
|
|
* Copyright (C) NGINX, Inc.
|
|
*/
|
|
|
|
#include <nxt_main.h>
|
|
#include <sys/types.h>
|
|
#include <nxt_conf.h>
|
|
#include <nxt_clone.h>
|
|
|
|
|
|
#if (NXT_HAVE_CLONE_NEWUSER)
|
|
|
|
nxt_int_t nxt_clone_credential_setgroups(nxt_task_t *task, pid_t child_pid,
|
|
const char *str);
|
|
nxt_int_t nxt_clone_credential_map_set(nxt_task_t *task, const char* mapfile,
|
|
pid_t pid, nxt_int_t default_container, nxt_int_t default_host,
|
|
nxt_clone_credential_map_t *map);
|
|
nxt_int_t nxt_clone_credential_map_write(nxt_task_t *task, const char *mapfile,
|
|
pid_t pid, u_char *mapinfo);
|
|
|
|
|
|
nxt_int_t
|
|
nxt_clone_credential_setgroups(nxt_task_t *task, pid_t child_pid,
|
|
const char *str)
|
|
{
|
|
int fd, n;
|
|
u_char *p, *end;
|
|
u_char path[PATH_MAX];
|
|
|
|
end = path + PATH_MAX;
|
|
p = nxt_sprintf(path, end, "/proc/%d/setgroups", child_pid);
|
|
*p = '\0';
|
|
|
|
if (nxt_slow_path(p == end)) {
|
|
nxt_alert(task, "error write past the buffer: %s", path);
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
fd = open((char *)path, O_RDWR);
|
|
|
|
if (fd == -1) {
|
|
/*
|
|
* If the /proc/pid/setgroups doesn't exists, we are
|
|
* safe to set uid/gid maps. But if the error is anything
|
|
* other than ENOENT, then we should abort and let user know.
|
|
*/
|
|
|
|
if (errno != ENOENT) {
|
|
nxt_alert(task, "open(%s): %E", path, nxt_errno);
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
n = write(fd, str, strlen(str));
|
|
close(fd);
|
|
|
|
if (nxt_slow_path(n == -1)) {
|
|
nxt_alert(task, "write(%s): %E", path, nxt_errno);
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
|
|
nxt_int_t
|
|
nxt_clone_credential_map_write(nxt_task_t *task, const char *mapfile,
|
|
pid_t pid, u_char *mapinfo)
|
|
{
|
|
int len, mapfd;
|
|
u_char *p, *end;
|
|
ssize_t n;
|
|
u_char buf[256];
|
|
|
|
end = buf + sizeof(buf);
|
|
|
|
p = nxt_sprintf(buf, end, "/proc/%d/%s", pid, mapfile);
|
|
if (nxt_slow_path(p == end)) {
|
|
nxt_alert(task, "writing past the buffer");
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
*p = '\0';
|
|
|
|
mapfd = open((char*)buf, O_RDWR);
|
|
if (nxt_slow_path(mapfd == -1)) {
|
|
nxt_alert(task, "failed to open proc map (%s) %E", buf, nxt_errno);
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
len = nxt_strlen(mapinfo);
|
|
|
|
n = write(mapfd, (char *)mapinfo, len);
|
|
if (nxt_slow_path(n != len)) {
|
|
|
|
if (n == -1 && nxt_errno == EINVAL) {
|
|
nxt_alert(task, "failed to write %s: Check kernel maximum " \
|
|
"allowed lines %E", buf, nxt_errno);
|
|
|
|
} else {
|
|
nxt_alert(task, "failed to write proc map (%s) %E", buf,
|
|
nxt_errno);
|
|
}
|
|
|
|
close(mapfd);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
close(mapfd);
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
|
|
nxt_int_t
|
|
nxt_clone_credential_map_set(nxt_task_t *task, const char* mapfile, pid_t pid,
|
|
nxt_int_t default_container, nxt_int_t default_host,
|
|
nxt_clone_credential_map_t *map)
|
|
{
|
|
u_char *p, *end, *mapinfo;
|
|
nxt_int_t ret, len;
|
|
nxt_uint_t i;
|
|
|
|
/*
|
|
* uid_map one-entry size:
|
|
* alloc space for 3 numbers (32bit) plus 2 spaces and \n.
|
|
*/
|
|
len = sizeof(u_char) * (10 + 10 + 10 + 2 + 1);
|
|
|
|
if (map->size > 0) {
|
|
len = len * map->size + 1;
|
|
|
|
mapinfo = nxt_malloc(len);
|
|
if (nxt_slow_path(mapinfo == NULL)) {
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
p = mapinfo;
|
|
end = mapinfo + len;
|
|
|
|
for (i = 0; i < map->size; i++) {
|
|
p = nxt_sprintf(p, end, "%L %L %L", map->map[i].container,
|
|
map->map[i].host, map->map[i].size);
|
|
|
|
if (nxt_slow_path(p == end)) {
|
|
nxt_alert(task, "write past the mapinfo buffer");
|
|
nxt_free(mapinfo);
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
if (i + 1 < map->size) {
|
|
*p++ = '\n';
|
|
|
|
} else {
|
|
*p = '\0';
|
|
}
|
|
}
|
|
|
|
} else {
|
|
mapinfo = nxt_malloc(len);
|
|
if (nxt_slow_path(mapinfo == NULL)) {
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
end = mapinfo + len;
|
|
p = nxt_sprintf(mapinfo, end, "%d %d 1",
|
|
default_container, default_host);
|
|
*p = '\0';
|
|
|
|
if (nxt_slow_path(p == end)) {
|
|
nxt_alert(task, "write past mapinfo buffer");
|
|
nxt_free(mapinfo);
|
|
return NXT_ERROR;
|
|
}
|
|
}
|
|
|
|
ret = nxt_clone_credential_map_write(task, mapfile, pid, mapinfo);
|
|
|
|
nxt_free(mapinfo);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
nxt_int_t
|
|
nxt_clone_credential_map(nxt_task_t *task, pid_t pid,
|
|
nxt_credential_t *app_creds, nxt_clone_t *clone)
|
|
{
|
|
nxt_int_t ret;
|
|
nxt_int_t default_host_uid;
|
|
nxt_int_t default_host_gid;
|
|
const char *rule;
|
|
nxt_runtime_t *rt;
|
|
|
|
rt = task->thread->runtime;
|
|
|
|
if (rt->capabilities.setid) {
|
|
rule = "allow";
|
|
|
|
/*
|
|
* By default we don't map a privileged user
|
|
*/
|
|
default_host_uid = app_creds->uid;
|
|
default_host_gid = app_creds->base_gid;
|
|
} else {
|
|
rule = "deny";
|
|
|
|
default_host_uid = nxt_euid;
|
|
default_host_gid = nxt_egid;
|
|
}
|
|
|
|
ret = nxt_clone_credential_map_set(task, "uid_map", pid, app_creds->uid,
|
|
default_host_uid,
|
|
&clone->uidmap);
|
|
|
|
if (nxt_slow_path(ret != NXT_OK)) {
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
ret = nxt_clone_credential_setgroups(task, pid, rule);
|
|
if (nxt_slow_path(ret != NXT_OK)) {
|
|
nxt_alert(task, "failed to write /proc/%d/setgroups", pid);
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
ret = nxt_clone_credential_map_set(task, "gid_map", pid, app_creds->base_gid,
|
|
default_host_gid,
|
|
&clone->gidmap);
|
|
|
|
if (nxt_slow_path(ret != NXT_OK)) {
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
|
|
nxt_int_t
|
|
nxt_clone_vldt_credential_uidmap(nxt_task_t *task,
|
|
nxt_clone_credential_map_t *map, nxt_credential_t *creds)
|
|
{
|
|
nxt_int_t id;
|
|
nxt_uint_t i;
|
|
nxt_runtime_t *rt;
|
|
nxt_clone_map_entry_t m;
|
|
|
|
if (map->size == 0) {
|
|
return NXT_OK;
|
|
}
|
|
|
|
rt = task->thread->runtime;
|
|
|
|
if (!rt->capabilities.setid) {
|
|
if (nxt_slow_path(map->size > 1)) {
|
|
nxt_log(task, NXT_LOG_NOTICE, "\"uidmap\" field has %d entries "
|
|
"but unprivileged unit has a maximum of 1 map.",
|
|
map->size);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
id = map->map[0].host;
|
|
|
|
if (nxt_slow_path((nxt_uid_t) id != nxt_euid)) {
|
|
nxt_log(task, NXT_LOG_NOTICE, "\"uidmap\" field has an entry for "
|
|
"host uid %d but unprivileged unit can only map itself "
|
|
"(uid %d) into child namespaces.", id, nxt_euid);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
for (i = 0; i < map->size; i++) {
|
|
m = map->map[i];
|
|
|
|
if (creds->uid >= (nxt_uid_t) m.container
|
|
&& creds->uid < (nxt_uid_t) (m.container + m.size))
|
|
{
|
|
return NXT_OK;
|
|
}
|
|
}
|
|
|
|
nxt_log(task, NXT_LOG_NOTICE, "\"uidmap\" field has no \"container\" "
|
|
"entry for user \"%s\" (uid %d)", creds->user, creds->uid);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
|
|
nxt_int_t
|
|
nxt_clone_vldt_credential_gidmap(nxt_task_t *task,
|
|
nxt_clone_credential_map_t *map, nxt_credential_t *creds)
|
|
{
|
|
nxt_uint_t base_ok, gid_ok, gids_ok;
|
|
nxt_uint_t i, j;
|
|
nxt_runtime_t *rt;
|
|
nxt_clone_map_entry_t m;
|
|
|
|
rt = task->thread->runtime;
|
|
|
|
if (!rt->capabilities.setid) {
|
|
if (creds->ngroups > 0
|
|
&& !(creds->ngroups == 1 && creds->gids[0] == creds->base_gid)) {
|
|
nxt_log(task, NXT_LOG_NOTICE,
|
|
"unprivileged unit disallow supplementary groups for "
|
|
"new namespace (user \"%s\" has %d group%s).",
|
|
creds->user, creds->ngroups,
|
|
creds->ngroups > 1 ? "s" : "");
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
if (map->size == 0) {
|
|
return NXT_OK;
|
|
}
|
|
|
|
if (nxt_slow_path(map->size > 1)) {
|
|
nxt_log(task, NXT_LOG_NOTICE, "\"gidmap\" field has %d entries "
|
|
"but unprivileged unit has a maximum of 1 map.",
|
|
map->size);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
m = map->map[0];
|
|
|
|
if (nxt_slow_path((nxt_gid_t) m.host != nxt_egid)) {
|
|
nxt_log(task, NXT_LOG_ERR, "\"gidmap\" field has an entry for "
|
|
"host gid %L but unprivileged unit can only map itself "
|
|
"(gid %d) into child namespaces.", m.host, nxt_egid);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
if (nxt_slow_path(m.size > 1)) {
|
|
nxt_log(task, NXT_LOG_ERR, "\"gidmap\" field has an entry with "
|
|
"\"size\": %L, but for unprivileged unit it must be 1.",
|
|
m.size);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
if (nxt_slow_path((nxt_gid_t) m.container != creds->base_gid)) {
|
|
nxt_log(task, NXT_LOG_ERR,
|
|
"\"gidmap\" field has no \"container\" entry for gid %d.",
|
|
creds->base_gid);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
if (map->size == 0) {
|
|
if (creds->ngroups > 0
|
|
&& !(creds->ngroups == 1 && creds->gids[0] == creds->base_gid))
|
|
{
|
|
nxt_log(task, NXT_LOG_ERR, "\"gidmap\" field has no entries "
|
|
"but user \"%s\" has %d suplementary group%s.",
|
|
creds->user, creds->ngroups,
|
|
creds->ngroups > 1 ? "s" : "");
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
base_ok = 0;
|
|
gids_ok = 0;
|
|
|
|
for (i = 0; i < creds->ngroups; i++) {
|
|
gid_ok = 0;
|
|
|
|
for (j = 0; j < map->size; j++) {
|
|
m = map->map[j];
|
|
|
|
if (!base_ok && creds->base_gid >= (nxt_gid_t) m.container
|
|
&& creds->base_gid < (nxt_gid_t) (m.container + m.size))
|
|
{
|
|
base_ok = 1;
|
|
}
|
|
|
|
if (creds->gids[i] >= (nxt_gid_t) m.container
|
|
&& creds->gids[i] < (nxt_gid_t) (m.container + m.size))
|
|
{
|
|
gid_ok = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nxt_fast_path(gid_ok)) {
|
|
gids_ok++;
|
|
}
|
|
}
|
|
|
|
if (!base_ok) {
|
|
for (i = 0; i < map->size; i++) {
|
|
m = map->map[i];
|
|
|
|
if (creds->base_gid >= (nxt_gid_t) m.container
|
|
&& creds->base_gid < (nxt_gid_t) (m.container + m.size))
|
|
{
|
|
base_ok = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nxt_slow_path(!base_ok)) {
|
|
nxt_log(task, NXT_LOG_ERR, "\"gidmap\" field has no \"container\" "
|
|
"entry for gid %d.", creds->base_gid);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
if (nxt_slow_path(gids_ok < creds->ngroups)) {
|
|
nxt_log(task, NXT_LOG_ERR, "\"gidmap\" field has missing "
|
|
"suplementary gid mappings (found %d out of %d).", gids_ok,
|
|
creds->ngroups);
|
|
|
|
return NXT_ERROR;
|
|
}
|
|
|
|
return NXT_OK;
|
|
}
|
|
|
|
#endif
|