Skip to content
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

chrootenv: resolve #32802, #32876, #32877, #32878 #32899

Merged
merged 5 commits into from Dec 22, 2017

Conversation

lukateras
Copy link
Member

@lukateras lukateras commented Dec 20, 2017

Motivation for this change

Formatted via clang-format. Valgrind reports "all heap blocks were freed". Steam still works!

Things done
  • Tested using sandboxing (nix.useSandbox on NixOS, or option build-use-sandbox in nix.conf on non-NixOS)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via one or more NixOS test(s) if existing and applicable for the change (look inside nixos/tests)
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nox --run "nox-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Fits CONTRIBUTING.md.

@lukateras lukateras changed the title chrootenv: resolve #32876, #32877, #32878 chrootenv: resolve #32802, #32876, #32877, #32878 Dec 20, 2017
@lukateras lukateras requested a review from Mic92 December 20, 2017 19:33
Mic92
Mic92 previously requested changes Dec 20, 2017
if ((env = getenv(names[i]))) {
if (asprintf(ptr++, "%s=%s", names[i], env) < 0)
errorf(EX_OSERR, "asprintf");
#define LEN(x) sizeof(x) / sizeof(*x)
Copy link
Member

Choose a reason for hiding this comment

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

nitpick:

- #define LEN(x) sizeof(x) / sizeof(*x)
+ #define LEN(x) (sizeof(x) / sizeof(*x))

char *env_blacklist[] = {};

char **env_filter(char *envp[]) {
char **filtered_envp = malloc(sizeof(*envp));
Copy link
Member

Choose a reason for hiding this comment

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

missing check if malloc failed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks!

char *env_blacklist[] = {};

char **env_filter(char *envp[]) {
char **filtered_envp = malloc(sizeof(*envp));
Copy link
Member

Choose a reason for hiding this comment

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

missing check if malloc failed. Consider to use calloc, zero initialized memory is usually easier to debug.

bool blacklisted = false;

for (size_t i = 0; i < LEN(env_blacklist); i++) {
if (!strncmp(*envp, env_blacklist[i], strlen(env_blacklist[i]))) {
Copy link
Member

Choose a reason for hiding this comment

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

What is the gain of strncmp here? You compute strlen first based on env_blacklist[i].

Copy link
Member Author

Choose a reason for hiding this comment

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

Because I only need to partially match the string. Example of valid entry in env_blacklist would be PWD=, example of *envp that would be blacklisted is PWD=/home/user.

Copy link
Contributor

Choose a reason for hiding this comment

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

The second parameter should be the index of the first '=' in the string indeed.

Copy link
Member Author

Choose a reason for hiding this comment

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

@dezgeg I don't think I understand. Could you elaborate?

Copy link
Contributor

@dezgeg dezgeg Dec 20, 2017

Choose a reason for hiding this comment

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

Sorry, that comment was a brain fart. But consider the case where env_blacklist contains FOO and *envp is FOOBAR.

Copy link
Member Author

@lukateras lukateras Dec 20, 2017

Choose a reason for hiding this comment

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

Yeah, the point was that env_blacklist elements would always end with an equal sign. But unsetenv approach is much better. Also I've discovered environ only today.

char **filtered_envp = malloc(sizeof(*envp));
size_t n = 0;

while (*envp != NULL) {
Copy link
Member

Choose a reason for hiding this comment

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

a for loop incrementing envp would be slightly more readable here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed!

if (path == NULL)
errorf(EX_OSERR, "malloc");

if (strcpy(path, dir) < 0)
Copy link
Member

Choose a reason for hiding this comment

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

path is not freed.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should not be freed, the point is to allocate enough memory to hold combined string to reduce boilerplate. free() happens at other places.

Copy link
Contributor

Choose a reason for hiding this comment

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

strcpy() can't fail, btw.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks!

if (strcpy(path, dir) < 0)
errorf(EX_IOERR, "strcpy");

if (strcat(path, name) < 0)
Copy link
Member

Choose a reason for hiding this comment

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

path is not freed

Copy link
Member Author

Choose a reason for hiding this comment

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

See above.

Copy link
Contributor

Choose a reason for hiding this comment

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

Neither can strcat().


bool isdir(char *path) {
struct stat buf;
stat(path, &buf);
Copy link
Member

Choose a reason for hiding this comment

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

Should check return value of stat -> buf could even contain the result of previous values stat calls.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right, I'll reimplement this on top of opendir.

char *host_target = strjoin("host/", prefix_dirent->d_name);

if (symlink(host_target, prefix_dirent->d_name) < 0)
errorf(EX_IOERR, "symlink");
Copy link
Member

Choose a reason for hiding this comment

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

Directory is not closed and memory is not freed in error case.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't understand why this has to be done. I'll read up on it.

Copy link
Member Author

@lukateras lukateras Dec 21, 2017

Choose a reason for hiding this comment

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

I think this should be handled by exit, not manually. Directories, files and memory are handled by default, and if there's something that is not handled in a larger program, atexit callback should be used.

@@ -170,7 +239,8 @@ int main(int argc, char *argv[]) {
if (waitpid(cpid, &status, 0) < 0)
errorf(EX_OSERR, "waitpid");

if (nftw(root, nftw_rm, getdtablesize(), FTW_DEPTH | FTW_MOUNT | FTW_PHYS) < 0)
if (nftw(root, nftw_rm, getdtablesize(), FTW_DEPTH | FTW_MOUNT | FTW_PHYS) <
0)
Copy link
Member

Choose a reason for hiding this comment

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

Write return code to a local variable instead of line wrapping here for readability.

Copy link
Member Author

Choose a reason for hiding this comment

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

Line wrapping will go away if I rename nftw_rm.

while (*envp != NULL) {
bool blacklisted = false;

for (size_t i = 0; i < LEN(env_blacklist); i++) {
Copy link
Contributor

@dezgeg dezgeg Dec 20, 2017

Choose a reason for hiding this comment

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

LEN() returns something completely wrong here Ok it's correct after all.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so:

$ cat example.c
#include <stdio.h>

#define LEN(x) sizeof(x) / sizeof(*x)

char *zzz[] = {"a", "b", "c"};

int main() {
  printf("%lu\n", LEN(zzz));
  return 0;
}
$ nix-shell -p gcc gnumake --run 'make example'
$ ./example
3

@@ -161,7 +230,7 @@ int main(int argc, char *argv[]) {

argv++;

if (execvpe(*argv, argv, env_build(env_whitelist, LEN(env_whitelist))) < 0)
if (execvpe(*argv, argv, env_filter(environ)) < 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the code for the blacklist approach would be much simpler if you just used execvp() and just modified the current environment with unsetenv().

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, I'm going to do that. Thanks!

errorf(EX_OSERR, "mount");
}

char *strjoin(char *dir, char *name) {
Copy link
Contributor

@dezgeg dezgeg Dec 20, 2017

Choose a reason for hiding this comment

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

This whole function could just be asprintf(&ret, "%s%s", dir, name)

or in C++: dir + name. ;)

Copy link
Member Author

@lukateras lukateras Dec 20, 2017

Choose a reason for hiding this comment

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

I know it could be asprintf, but it's cleaner this way. Check previous chrootenv version, I've used asprintf.

Copy link
Contributor

Choose a reason for hiding this comment

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

How is it cleaner to write 13 lines of code to replace 1 line?

Copy link
Member Author

@lukateras lukateras Dec 20, 2017

Choose a reason for hiding this comment

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

It has better semantics.

There is no template, only concatenation, while asprintf has its own DSL.

Copy link
Contributor

Choose a reason for hiding this comment

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

But every C programmer who reads asprintf(&ret, "%s%s", x, y) immediately knows it's just concatenating two strings.

And in fact you're using this in precisely one place, with a constant string as the other arg. Just

char *host_target = strjoin("host/", prefix_dirent->d_name)

->

char *host_target;
asprintf(&host_target, "host/%s", prefix_dirent->d_name);

to make it even more simpler.

Copy link
Member Author

@lukateras lukateras Dec 21, 2017

Choose a reason for hiding this comment

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

OK, point taken. errorf uses the same DSL anyway so it seems there is not much use in implementing simpler asprintf, at least not without dropping errorf.

return path;
}

char *bind_blacklist[] = {".", "..", "bin", "etc", "host", "usr"};
Copy link
Contributor

Choose a reason for hiding this comment

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

const char * const bind_blacklist[] etc.

errorf(EX_OSERR, "asprintf");
#define LEN(x) sizeof(x) / sizeof(*x)

char *env_blacklist[] = {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this variable totally unused?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah it's supposed to be a constant... const char * const then.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's unused for now.

This variable is supposed to be filled in presumably when @abbradar gets better.

Copy link
Member

Choose a reason for hiding this comment

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

Then I would add a TODO comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do!


if (setenv("NIX_CHROOTENV", "1", false) < 0)
errorf(EX_IOERR, "setenv");

char tmpl[] = "/tmp/chrootenvXXXXXX";
char *root = mkdtemp(tmpl);
Copy link
Member

@Mic92 Mic92 Dec 21, 2017

Choose a reason for hiding this comment

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

root is not removed in the error case.

* Wrap LEN macro in parantheses
* Drop env_filter in favor of stateful environ_blacklist_filter,
  use execvp instead of execvpe, don't explicitly use environ
* Add argument error logging wherever it makes sense
* Drop strjoin in favor of asprintf
* char* -> const char* where appropriate
* Handle stat errors
* Print user messages with fputs, not errorf
* Abstract away is_str_in (previously bind_blacklisted)
* Cleanup temporary directory on error
* Some minor syntactic and naming changes

Thanks to Jörg Thalheim and Tuomas Tynkkynen for the code review!
@lukateras
Copy link
Member Author

lukateras commented Dec 21, 2017

@Mic92, @dezgeg: again, thanks for the review. I've fixed most of the problems that you told me about in 08156be (see commit log) except where I explicitly disagree:

  • I agree that it's preferable to free memory after it has been used but I don't think that memory (or any other resource) should be explicitly handled before exit. Kernel mostly does that for us.
  • I disagree with @Mic92 in chrootenv: resolve #32802, #32876, #32877, #32878 #32899 (comment): I believe that function name is more informative and better for error reports. However, I've implemented argument logging. Also, now user-targeted messages are printed via fputs and not errorf, i.e. without source filename, line and strerror, and on a separate line. Before:
    $ ./chrootenv ls
    chrootenv.c:100: unshare: run `sudo sysctl -w kernel.unprivileged_userns_clone=1`: operation not permitted
    
    After:
    $ ./chrootenv ls
    Run: sudo sysctl -w kernel.unprivileged_userns_clone=1
    chrootenv.c:100: unshare: operation not permitted
    
  • clang-format might have its quirks, but it's a standard way to indent C code. It might handle one line out of 250 rather badly, but on average, it indents code way better than most people do / have time for.

@lukateras lukateras dismissed Mic92’s stale review December 21, 2017 23:48

Taken into consideration

@lukateras
Copy link
Member Author

I plan to merge this tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants