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

Initial implementation of landlock calls #903

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

Riolku
Copy link
Contributor

@Riolku Riolku commented Sep 11, 2021

No description provided.

@dmoj-build
Copy link
Collaborator

Can one of the admins verify this patch?

@codecov-commenter
Copy link

codecov-commenter commented Sep 11, 2021

Codecov Report

Base: 81.44% // Head: 84.17% // Increases project coverage by +2.72% 🎉

Coverage data is based on head (244c4a1) compared to base (1f17cd2).
Patch coverage: 85.71% of modified lines in pull request are covered.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #903      +/-   ##
==========================================
+ Coverage   81.44%   84.17%   +2.72%     
==========================================
  Files         137      137              
  Lines        4920     4953      +33     
==========================================
+ Hits         4007     4169     +162     
+ Misses        913      784     -129     
Impacted Files Coverage Δ
dmoj/cptbox/__init__.py 100.00% <ø> (ø)
dmoj/executors/base_executor.py 86.86% <ø> (ø)
dmoj/cptbox/compiler_isolate.py 56.60% <71.42%> (+7.66%) ⬆️
dmoj/cptbox/tracer.py 76.82% <84.21%> (+16.39%) ⬆️
dmoj/cptbox/isolate.py 89.75% <100.00%> (+38.53%) ⬆️
dmoj/executors/RUST.py 100.00% <100.00%> (ø)
dmoj/executors/SCALA.py 95.74% <100.00%> (+0.09%) ⬆️
dmoj/judge.py 54.68% <100.00%> (+1.34%) ⬆️
dmoj/result.py 83.11% <0.00%> (-1.30%) ⬇️
... and 6 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@kiritofeng
Copy link
Member

ok to test

dmoj/cptbox/_cptbox.pyx Outdated Show resolved Hide resolved
dmoj/cptbox/_cptbox.pyx Outdated Show resolved Hide resolved
dmoj/cptbox/helper.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/landlock_helper.c Outdated Show resolved Hide resolved
dmoj/cptbox/landlock_helper.c Outdated Show resolved Hide resolved
dmoj/cptbox/landlock_helper.c Outdated Show resolved Hide resolved
dmoj/cptbox/landlock_helper.c Outdated Show resolved Hide resolved
dmoj/cptbox/landlock_helper.c Outdated Show resolved Hide resolved
dmoj/cptbox/helper.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/ptproc.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/helper.h Outdated Show resolved Hide resolved
dmoj/cptbox/ptbox.h Outdated Show resolved Hide resolved
@Riolku Riolku force-pushed the landlock-impl branch 12 times, most recently from e2644de to 7f33dfa Compare September 15, 2021 17:02
@Riolku
Copy link
Contributor Author

Riolku commented Sep 15, 2021

Landlock applies LANDLOCK_FS_READ_DIR recursively. This means that since we allow READ_DIR /, a process can search the whole filesystem tree. We can limit getdents, but a process can still arbitrarily determine the existence of a file/directory. This might be acceptable.

@Riolku Riolku force-pushed the landlock-impl branch 5 times, most recently from 1bdd49c to 7c5316f Compare September 17, 2021 04:14
@Riolku Riolku marked this pull request as ready for review September 17, 2021 04:18
@Riolku
Copy link
Contributor Author

Riolku commented Sep 17, 2021

Given that stuff is now functional, we can begin to review this.

dmoj/cptbox/_cptbox.pyx Outdated Show resolved Hide resolved
dmoj/cptbox/_cptbox.pyx Outdated Show resolved Hide resolved
dmoj/cptbox/helper.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/helper.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/helper.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/landlock_header.h Show resolved Hide resolved
dmoj/cptbox/landlock_helpers.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/landlock_helpers.cpp Outdated Show resolved Hide resolved
dmoj/cptbox/tracer.py Outdated Show resolved Hide resolved
dmoj/cptbox/isolate.py Outdated Show resolved Hide resolved
@Riolku
Copy link
Contributor Author

Riolku commented Jan 3, 2023

Landlock ABI 3 shipped in 5.19 which I think is able to supplant our syscall simulation here.

We should hard-require it; @quantum5 is working on making a 6.x kernel available in the ARM64 CI runner.

Would this also mean we can do away with some of the header files that we copied? landlock_header.h specifically.

@Xyene
Copy link
Member

Xyene commented Jan 3, 2023

No, we need them to be able to do the initial Landlock calls to figure out if Landlock is supported at all.

A helper header is provided so as to avoid issues with missing headers,
so we can compile unconditionally.
@Riolku Riolku force-pushed the landlock-impl branch 2 times, most recently from a2c7e1e to e8f4ed0 Compare January 4, 2023 04:49
One key note to make here is that WRITE permissions imply READ permissions, because otherwise, calls with O_RDWR will fail even if one rule grants read and another grants write.

Another thing to note is that under landlock, only the main process can
access /proc. This is because we can't add rules when the children
spawn.
Note that under the current version of landlock, some syscalls are not handled by landlock, including `stat` and `access`.
Under landlock, linking and renaming throw EXDEV if `src` and `dst` are not in the same directory. This is unacceptable for us, so we simulate the calls.

Because we are simulating, we want to make sure that `flags` is zero for
`linkat`, otherwise it's likely that we have done something unintended.
Under landlock, since /proc is not accessible in a subprocess, SCALA calls `mincore`. We allow this in order for it to pass.

Also, since `execve` is checked under landlock, we need to add `/bin` to the list of readable directories.
@Riolku Riolku force-pushed the landlock-impl branch 2 times, most recently from de66427 to d20bd11 Compare January 4, 2023 05:15

int landlock_version = get_landlock_version();
if (landlock_version < 2) {
// ABI not old enough. Skip to seccomp
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// ABI not old enough. Skip to seccomp
// ABI not new enough. Skip to seccomp

LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_REFER)) {
// landlock_add_rules logs errors
Copy link
Member

Choose a reason for hiding this comment

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

Could this comment have more details? I don't understand what it's referring to, is this comment unrelated to the following close?

return 0;
return -1;
#else
return 0; // FreeBSD does not have landlock
Copy link
Member

Choose a reason for hiding this comment

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

We should abort() here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How? Consumers of this function throw a python error, but what would be appropriate to do here?

Copy link
Member

Choose a reason for hiding this comment

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

Got it. I think has_landlock should be implemented here rather than in Python, so we can return false order FREEBSD.

#define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12)
#endif /* _LINUX_LANDLOCK_H */

// Not always defined, depends on ABI version.
Copy link
Member

Choose a reason for hiding this comment

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

ifndef?

@@ -0,0 +1,3 @@
#include "landlock_header.h"

int landlock_add_rules(const int ruleset_fd, const char **paths, __u64 access_rule);
Copy link
Member

Choose a reason for hiding this comment

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

Could we name this cptbox_landlock_add_rules? Makes it easier to keep track of what's provided by the kernel versus by us.


if (rule.parent_fd < 0) {
if (errno == ENOENT)
goto close_fd; // missing files are ignored
Copy link
Member

Choose a reason for hiding this comment

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

Nit: parens even around single-statement branches / loops / etc. I know not all cptbox code is written like this, but all new code should be.

if security is not None:
self.configure_files(security.read_fs, security.write_fs)
else:
self.configure_files([], [])
Copy link
Member

@Xyene Xyene Jan 8, 2023

Choose a reason for hiding this comment

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

Is this branch necessary at all? Seems a little weird to have.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unsure. security defaults to None. I don't know why.

Copy link
Member

Choose a reason for hiding this comment

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

My question was more: it seems weird that the default uninitialized state would be different from initializing with [].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could maybe avoid it by having a default of empty list in _cptbox.pyx, but I don't see how else to avoid it. configure_files is the function that sets the landlock_* attributes.

We could, in theory, have them defaulted to empty and then have this branch to set them to something, but I don't love that.

@@ -223,6 +244,8 @@ def wait(self) -> int:
raise RuntimeError('failed to spawn child')
elif self.returncode == PTBOX_SPAWN_FAIL_SETAFFINITY:
raise RuntimeError('failed to set child affinity')
elif self.returncode == PTBOX_SPAWN_FAIL_LANDLOCK:
raise RuntimeError('landlock configuration failed')
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
raise RuntimeError('landlock configuration failed')
raise RuntimeError('Landlock configuration failed')

Nit, here and in other user-facing strings as well as comments. The kernel docs refer to it as "Landlock" (despite "seccomp" being lowercased).

@@ -20,6 +20,7 @@ class Executor(JavaExecutor):
ExactFile('/bin/bash'),
RecursiveDir('/etc/alternatives'),
]
compiler_syscalls = ['mincore']
Copy link
Member

Choose a reason for hiding this comment

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

Why did this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SCALA takes a different path under landlock last I checked, and that path requires mincore. I believe it had to do with how landlock deals with /proc/<pid>.

We decided at some point to simply allow the call.

Copy link
Member

Choose a reason for hiding this comment

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

Ack, thanks for explaining. Could we split this out in its own commit?

Copy link
Member

Choose a reason for hiding this comment

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

Eh, I guess it kind of is already. Fine to leave it as-is.

int get_landlock_version() {
#if !PTBOX_FREEBSD
char *sandbox_mode = getenv("DMOJ_SANDBOX_MODE");
if (sandbox_mode != nullptr && strcmp(sandbox_mode, "ptrace+seccomp") == 0) {
Copy link
Member

Choose a reason for hiding this comment

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

This is a surprising place for this check to live, and I think the check should be stricter than this.

We should validate that the string is one of:

  • ptrace+seccomp
  • ptrace+seccomp+landlock
  • auto

and otherwise bail out. Otherwise we can't confidently make changes to the interpretation of this environment variable, as we'd have been too lax in its inputs in earlier versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that this location is strange. Where should it live?

Copy link
Member

Choose a reason for hiding this comment

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

I think living in this file is probably fine, and it should return an enum. Then it should be consulted in the spawn routine alongside the Landlock version we have available. My concern here is more about subtly overloading the semantics of get_landlock_version than anything else.

We should bail if someone requests:

  • seccomp or seccomp+landlock support on FreeBSD
  • ptrace-only support on Linux
  • seccomp+landlock on a Linux with too old a Landlock ABI

This sanity check could exist in judge.py alongside all the other sanity checks, but I don't feel strongly about that.

@@ -27,8 +28,16 @@ struct child_config {
int *seccomp_handlers;
// 64 cores ought to be enough for anyone.
unsigned long cpu_affinity_mask;
const char **landlock_read_exact_files;
Copy link
Member

Choose a reason for hiding this comment

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

This may end up looking a little cleaner if we have a

struct {
    const char **read_exact_files;
    ...
} landlock;

or even

const struct {
    char **read_exact_files;
    ...
} landlock;

assert self._executable is not None
# Under landlock we need this for execve to work.
# We use `self._executable` because it is copied when caching executors, but other properties are not.
return super().get_fs() + [ExactFile(self._executable)]
Copy link
Member

Choose a reason for hiding this comment

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

The fact that both get_executable and get_compiled_file exist but return possibly different values is an abomination, and we should fix this. You don't have to do so in this PR, but please leave a comment to the effect of this being a hack to work around that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem isn't because get_executable and get_compiled_file both exist. Indeed, I expect them to return the same file for RUST.

The reason they don't is because the cached executor isn't a full copy of the original. Instead, it simply sets _executable and _dir on the returned object, and nothing else.

For rust, that means that we try and lock a new directory, which is obviously wrong.

To bypass it I use _executable here, but the proper fix may be instead to look into the caching.

Here's the relevant line: https://github.com/DMOJ/judge-server/blob/master/dmoj/executors/compiled_executor.py#L58

Also, as for get_executable vs get_compiled_file, check out Java's usage of the methods:

https://github.com/DMOJ/judge-server/blob/master/dmoj/executors/java_executor.py#L82

Maybe these exist because Java's compiled file isn't the executable?

@@ -33,6 +33,7 @@
ExactFile('/dev/urandom'),
ExactFile('/dev/random'),
*USR_DIR,
RecursiveDir('/bin'), # required under landlock when /bin is not a symlink, since we check execve.
Copy link
Member

Choose a reason for hiding this comment

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

Fine to not leave a comment here.

return PTBOX_SPAWN_FAIL_LANDLOCK;
}

// Note: WRITE must imply READ. This is required because even if one rule allows writing and another allows reading,
Copy link
Member

Choose a reason for hiding this comment

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

The O_TRUNC handling in Landlock scares me. Could we add some testcases with a custom grader that make sure that we don't allow truncating etc.?

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

Successfully merging this pull request may close these issues.

7 participants