-
Notifications
You must be signed in to change notification settings - Fork 1
mac_do(4): Implemented new sysctl knob and jail param for allowed executable paths #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Kushagra Srivastava <[email protected]>
Signed-off-by: Kushagra Srivastava <[email protected]>
Signed-off-by: Kushagra Srivastava <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you said via Signal, there is indeed currently a leak because you're allocating twice the fields rules
and exec_paths
of struct conf
. First alloc happens via alloc_conf()
, second one occurs when you call parse_rules()
/clone_rules()
and parse_exec_paths()
/clone_exec_paths()
, where you are basically crushing the existing pointers (which point to the leaked memory).
Additionally, clone_rules()
only makes a shallow copy, whereas a deep copy is required (or else, you would have to have a ref count on struct rules
and struct exec_paths
also, but I suggest to give up on that for the time being).
More generally, I think you should just inline struct rules
and struct exec_paths
directly in struct conf
, as said in inline comments. This will simplify lifecycle problems for the time being, and help push this forward.
There are a lot more inline comments with suggested changes. Goal here is going further in ensuring that memory lifecycle only happens for struct conf
, and to avoid lots of allocations and data copying in various places (which in particular will remove practically all risks of bugs, such as the one explained above).
If you don't think you can handle all that in a reasonable timeframe (a few days), what I would propose is for you to fix the two outstanding issues (first two paragraphs above), and then I can handle the rest myself.
We could have some phone call on monday or tuesday about all that as necessary.
(Note for myself: There is also a delicate concurrency issue when copying the settings of the applicable configuration, to be handled once these changes are in.)
sys/security/mac_do/mac_do.c
Outdated
struct rules *rules; | ||
struct exec_paths *exec_paths; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be simpler to just include the structs inline here (i.e., just removing the *
).
sys/security/mac_do/mac_do.c
Outdated
char *rules_string, *exec_paths_string; | ||
int error, jsys, rules_len = 0, execs_len = 0; | ||
|
||
/* Read mac.do = -1 if unset */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I think you're meaning is that -1
is a sentinel value indicating an unspecified mode. This is somewhat redundant with the comment before the _Static_assert()
above, but I'm fine with having one here. I would instead just add /* Mark unfilled. */
after the jsys = -1;
statement below, as is done in mac_do_jail_set()
. Actually, /* Mark unspecified. */
here and there seems even better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, /* Read mac.do = -1 if unset */
is hard to understand without context. Maybe just remove this comment once you've added the one suggested above on the jsys = -1;
line, or if you think this needs more explanation, replace with something like /* If no mode is explicitly specified, 'jsys' is initialized to -1 and will be overridden with a valid value based on other parameters. */
(And general style: Always finish sentences with dots in comments, even for small comments.)
sys/security/mac_do/mac_do.c
Outdated
promote_inherited_conf(struct prison *pr, bool with_rules, bool with_execs) | ||
{ | ||
struct prison *ppr; | ||
struct conf *parent = find_conf(pr, &ppr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parent
misleading. Rename to, e.g., applicable_conf
.
sys/security/mac_do/mac_do.c
Outdated
error = parse_and_set_exec_paths(td_pr, buf, &parse_error); | ||
if (error != 0) { | ||
if (print_parse_error) | ||
printf("MAC/do: Parse error at index %zu: %s\n", | ||
parse_error->pos, parse_error->msg); | ||
free_parse_error(parse_error); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, you would use parse_and_set_conf()
here instead.
sys/security/mac_do/mac_do.c
Outdated
vfs_getopts(opts, "mac.do.exec_paths", &execs_err); | ||
|
||
if (rules_err == ENOENT && execs_err == ENOENT) | ||
set_default_conf(pr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing for the presence of "mac.do.rules"
and "mac.do.exec_paths"
is not a bad idea per se, but I would prefer to avoid it, as:
- Setting the default configuration (which disables
mac_do(4)
) at creation makes us immune to any changes in the jail machinery that would allow the jail being created to be observed before the parameters are set (not possible today, and very unlikely in the future). - This forces duplicating detection code. Typically here, you would also have to test whether strings are empty, consistently with what is done in other
mac_do_jail_*
functions.
Jails are not created often, and there is no real constraint performance-wise here.
So here I prefer less code (just callset_default_conf()
unconditionally) rather than duplicate one.
Signed-off-by: Kushagra Srivastava <[email protected]>
Signed-off-by: Kushagra Srivastava <[email protected]>
Multiple issues existed within the powerpc FP/VSX save/restore functionality, leading to register corruption and loss of register contents in specific scenarios involving high signal load and use of both floating point and VSX instructions. Issue #1 On little endian systems the PCB used the wrong location for the shadowed FP register within the larger VSX register. This appears to have been an attempt to correct issue #2 without understanding how the vector load/store instructions actually operate. Issue #2 On little endian systems, the VSX state save/restore routines swapped 32-bit words within the 64-bit aliased double word for the associated floating point register. This was due to the use of a word-oriented load/store vs. doubleword oriented load/store. Issue #3 The FPU was turned off in the PCB but not in hardware, leading to a potential race condition if the same thread was scheduled immediately after sigreturn. The triggering codebase for this is Go, which makes heavy use of signals and and generates an unusual mix of floating point and VSX assembler. As a result, when combined with th powerpc lazy FPU restore, a condition was repeatedly hit whereby the thread was interrupted in FP+VSX mode, then restored in FP only mode, thus reliably triggering the issues above. Also clean up the associated asm() style issue flagged by GitHub Actions. Signed-off-by: Timothy Pearson <[email protected]> MFC after: 1 week Pull Request: freebsd#1756
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is still a lot to tackle.
GitHub's reviews are not ideal, e.g., I don't think you can see the old comments on a new version of the diff, even if they are still applicable (i.e., have not been resolved). You can cycle through all comments by clicking on the comments icon (has two comics-like bubbles in it, with a number on the side), when on the "Files changed" tab.
Please be careful about reading all existing comments and fully understanding them.
sys/security/mac_do/mac_do.c
Outdated
char *rules_string, *exec_paths_string; | ||
int error, jsys, rules_len = 0, execs_len = 0; | ||
|
||
/* Read mac.do = -1 if unset */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, /* Read mac.do = -1 if unset */
is hard to understand without context. Maybe just remove this comment once you've added the one suggested above on the jsys = -1;
line, or if you think this needs more explanation, replace with something like /* If no mode is explicitly specified, 'jsys' is initialized to -1 and will be overridden with a valid value based on other parameters. */
(And general style: Always finish sentences with dots in comments, even for small comments.)
sys/security/mac_do/mac_do.c
Outdated
if (!has_rules && !has_execs) { | ||
vfs_opterror(opts, "mac.do set to 'new' but neither rules nor exec_paths specified"); | ||
return (EINVAL); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should indeed allow this case as described in my previous comment.
sys/security/mac_do/mac_do.c
Outdated
case JAIL_SYS_DISABLE: | ||
remove_conf(pr); | ||
return (0); | ||
|
||
struct prison *p; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've just removed remove_conf()
, which could be enough but provided mac_do_jail_create()
is changed as requested (see old comment for it).
sys/security/mac_do/mac_do.c
Outdated
break; | ||
/* Infer jsys if needed */ | ||
if (jsys == -1) { | ||
if (has_rules) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest changing with:
if (has_rules) | |
if (has_rules || has_exec_paths) |
i.e., if only exec paths are specified, we just copy the rules part. I think this is more consistent with the rest, even if it could be slightly more dangerous.
If you have a strong case that we should not, then at least please add a comment saying that not putting has_exec_paths
is deliberate and why.
sys/security/mac_do/mac_do.c
Outdated
if (parent == NULL) | ||
return (NULL); | ||
|
||
struct conf *new_conf = alloc_conf(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: Declaration must be at top of file.
sys/security/mac_do/mac_do.c
Outdated
{ | ||
struct prison *ppr; | ||
struct conf *parent = find_conf(pr, &ppr); | ||
prison_unlock(ppr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You must obtain a reference on conf
before releasing the prison lock here, else there is a risk that it is freed concurrently (e.g., if an administrator changes the settings of the upper jail) while we are reading from it. And you must release that reference when finished with conf
.
Style: Statements must be separated from the declarations by an empty line.
sys/security/mac_do/mac_do.c
Outdated
parse_error->pos, parse_error->msg); | ||
free_parse_error(parse_error); | ||
parent_conf = find_conf(curproc->p_ucred->cr_prison, &p); | ||
prison_unlock(p); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same problem here as explained in a comment I left in parse_and_set_conf()
, you must obtain a reference on conf
before releasing the prison lock.
sys/security/mac_do/mac_do.c
Outdated
} | ||
} | ||
|
||
prison_unlock(pr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prison_unlock()
call is wrongly placed, as it won't be executed if exec_path_count
is 0, and it has to be regardless of its value.
|
||
if (exec_paths->exec_path_count > 0) { | ||
for (int i = 0; i < exec_paths->exec_path_count; i++) { | ||
if (strcmp(exec_paths->exec_paths[i], path) == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note (mostly for me): This does not work inside jails in general, as vn_fullpath()
above returns the full path from the machine's root, not the one from the current jail. This is a pre-existing bug, which I didn't catch in testing as I only tested with child jails having the same root. You don't have to fix it yourself, but if you want to, then advance in path
after the jail's root prefix (obtained through cr_prison->pr_path
) before the loop with the strcmp()
calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's working in jails for me. My jail is stored in /root/jails/test, and I supplied "/usr/bin/mdo:/home/thesynthax/mdo" for mac.do.exec_paths, and still worked.
Signed-off-by: Kushagra Srivastava <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See inline comments for the leaks and breaking the "installed configuration (ref count non 0) should never be modified" invariant, which must be restored.
sys/security/mac_do/mac_do.c
Outdated
toast_rules(struct rules *const rules) | ||
toast_rules(struct rules const rules) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep passing a pointer (else the structure is copied without reason; this could even lead to bugs if, e.g., modifying the structure like zero-ing it (this is not the case currently)).
sys/security/mac_do/mac_do.c
Outdated
struct rules rules; | ||
|
||
_Static_assert(MAC_RULE_STRING_LEN > 0, "MAC_RULE_STRING_LEN <= 0!"); | ||
rules->string[0] = 0; | ||
STAILQ_INIT(&rules->head); | ||
rules->use_count = 0; | ||
bzero(&rules, sizeof(rules)); | ||
rules.string[0] = '\0'; | ||
STAILQ_INIT(&rules.head); | ||
|
||
return (rules); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it's a common idiom in lots of other languages, in C it's rare that a function takes a structure as an argument or returns a structure as these are just copied, which can lead to surprising behaviors (you modify a copy, not the original object; not the case here) and always to worse performance (sometimes it doesn't really matter, but in systems programming, it's better to avoid it always).
So, the idiom here is that you instead pass as an argument the pointer to the structure to modify, and modify the object through it (and you don't return anything (return type is void
), or possibly the same pointer you received, although here there's no point in doing that).
sys/security/mac_do/mac_do.c
Outdated
} | ||
|
||
static struct exec_paths | ||
init_exec_paths(void) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as for init_rules()
(signature + remove redundant code).
sys/security/mac_do/mac_do.c
Outdated
if (error != 0) { | ||
(*parse_error)->pos += rule - copy; | ||
toast_rules(rules); | ||
toast_rules(*rules); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained above:
toast_rules(*rules); | |
toast_rules(rules); |
sys/security/mac_do/mac_do.c
Outdated
if (refcount_release(&rules->use_count)) | ||
toast_rules(rules); | ||
if (refcount_release(&conf->use_count)) { | ||
toast_rules(conf->rules); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toast_rules(conf->rules); | |
toast_rules(&conf->rules); |
sys/security/mac_do/mac_do.c
Outdated
bzero(&rules, sizeof(rules)); | ||
rules.string[0] = '\0'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just drop these two lines: The second is redundant with the first, and the first is redundant when assuming that the provided storage has been zeroed.
bzero(&rules, sizeof(rules)); | |
rules.string[0] = '\0'; |
Add an herald comment before init_rules()
saying it assumes the storage has been zeroed already.
sys/security/mac_do/mac_do.c
Outdated
prison_unlock(ppr); | ||
|
||
if (ppr == pr) | ||
conf = applicable_conf; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this line and what happens below, you're breaking the invariant that, once allocated, configurations should never be modified. This is very important for correctness when another thread is concurrently executing a credentials-changing function. So you first have to duplicate applicable_conf
in this case.
conf = alloc_conf(); | ||
|
||
if (rules_string != NULL && rules_string[0] != '\0') { | ||
error = parse_rules(rules_string, &conf->rules, parse_error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your leak is here (and below for exec paths, and in the similar duplicated code in mac_do_jail_set()
that should disappear) when conf
is applicable_conf
, see my comment above on how to fix.
conf->rules = clone_rules(&applicable_conf->rules); | ||
|
||
if (exec_paths_string != NULL && exec_paths_string[0] != '\0') { | ||
error = parse_exec_paths(exec_paths_string, &conf->exec_paths, parse_error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leak here, see comment above about how to fix.
} | ||
|
||
static int | ||
parse_and_set_conf(struct prison *pr, const char *rules_string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add here the optimization of not retrieving the applicable conf when at least one of the rules and exec paths strings are not NULL
.
Signed-off-by: Kushagra Srivastava <[email protected]>
Signed-off-by: Kushagra Srivastava <[email protected]>
Signed-off-by: Kushagra Srivastava <[email protected]>
The current incarnation of execvPe() is a bit messy, and it can be rather difficult to reason about whether we're actually doing the right thing with our errors. We have two cases in which we may enter the loop: 1.) We have a name that has no slashes in it, and we enter the loop normally through our strsep() logic to process $PATH 2.) We have a name with at least one slash, in which case we jump into the middle of the loop then bail after precisely the one iteration if we failed Both paths will exit the loop if we failed, either via jumping to the `done` label to preserve an errno or into the path that clobbers errno. Clobbering errno for case #2 above would seem to be wrong, as we did not actually search -- this would seem to be what POSIX expects, as well, based on expectations of the conformance test suite. Simplify reasoning about the two paths by splitting out an execvPe_prog that does the execve(2) call specifically, and returns based on whether the error would be fatal in a PATH search or not. For the relative/absolute case, we can just ignore the return value and keep errno intact. The search case gets simplified to return early if we hit a fatal error, or continue until the end and clobber errno if we did not find a suitable candidate. Another posix_spawnp() test is added to confirm that we didn't break our EACCES behavior in the process. Reviewed by: des, markj Sponsored by: Klara, Inc. Differential Revision: https://reviews.freebsd.org/D51629
Thank you for taking the time to contribute to FreeBSD! |
9c25634
to
14fdc49
Compare
Features: