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

SDL: Add option to drop privileges with unveil()/pledge() #1271

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

Conversation

bentley
Copy link
Contributor

@bentley bentley commented Jan 18, 2019

pledge() and unveil() are two OpenBSD APIs to restrict the capabilities of a program. pledge whitelists the set of syscalls the program can call, while unveil whitelists a view of the filesystem to the program.

pledge can be called repeatedly to reduce the list of allowed syscalls even further. This patch calls pledge twice, initially with a very broad list and finally with "stdio rpath wpath cpath fattr" (for savestates) "drm" (for graphics) "audio" (for audio). The debuggers additionally need "tty" (-d) and "net" (-g). The kernel kills a program that violates its promises. Obviously if that ever happens, the arguments to pledge need to be fixed.

unveil is called once per file or directory to whitelist, with a specified permission (read/write/execute/create), and a final call with null arguments to prevent further whitelisting. Attempts to access anything not unveiled result in ENOENT or EACCES. The kernel’s method of keeping track of these files is somewhat expensive, so typical practice is to call unveil as late as possible with as few files as possible. Here I only pass it the savestates.

Libepoxy is not compatible with these pledges; its deferred dlopen() calls cause the the program to be killed on glDeleteTextures(). I disabled it conditionally in CMake.

I’ve tested normal gameplay, USB gamepad, networked sndio audio, and basic -d usage (read: started program and hit “c”).

Questions:

  • Right now only the base names of the savestates are unveiled, so they only work if the ROM is in $PWD. How do I pass the directory through to the SDL frontend?
  • Any obvious file accesses or syscalls that I missed? Particularly in the debuggers?
  • Any style issues?

@bentley bentley force-pushed the pledge-unveil branch 2 times, most recently from 8ba3c7b to e74e5de Compare January 19, 2019 07:43
@bentley
Copy link
Contributor Author

bentley commented Jan 19, 2019

New revision, pulling in the savestate path the same way savestate writes do. (Kosher?) unveil works now for a ROM in any location.

@endrift
Copy link
Member

endrift commented Jan 19, 2019

New commit will fail with other VDir types, like ZIP files.

@bentley
Copy link
Contributor Author

bentley commented Jan 19, 2019

I expected that, but was surprised to find it actually worked for zip files.

@endrift
Copy link
Member

endrift commented Jan 19, 2019

That's not reliable though, it just happens to be that the struct elements were in the right order for that to work out.

@bentley
Copy link
Contributor Author

bentley commented Jan 19, 2019

Bummer… reverted. So hooking the VDir creation routines will be the next step.

@bentley
Copy link
Contributor Author

bentley commented May 23, 2020

Clearly I’m not going to get around to finishing unveil support soon… however, the pledge support works well, and has been part of the OpenBSD package for over a year, including the last three OpenBSD releases. So let’s reduce the scope of this pull request to pledge’s syscall filtering, which is ready to go in. What are your thoughts?

@endrift
Copy link
Member

endrift commented May 23, 2020

I'll take a more in-depth look tomorrow (it's 4 AM here, I shouldn't still be awake) but you should probably start by restricting the status output and checks in CMake to just being on OSes that have it (OpenBSD and...SerenityOS I think?)

@bentley
Copy link
Contributor Author

bentley commented May 23, 2020

I think SerenityOS shows up as “Generic” in CMake and isn’t otherwise detectable, but I don’t know for sure. Because of that I only gated pledge() support behind a variable that defaults to off. I can change that to if (CMAKE_SYSTEM_NAME STREQUAL OpenBSD) if you want though.

CMakeLists.txt Show resolved Hide resolved
CMakeLists.txt Outdated Show resolved Hide resolved
@@ -149,6 +154,15 @@ int main(int argc, char** argv) {
renderer.player.bindings = &renderer.core->inputMap;
mSDLInitBindingsGBA(&renderer.core->inputMap);
mSDLInitEvents(&renderer.events);

#ifdef USE_PLEDGE
if (!mPledgeBroad(&args)) {
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason this isn't done immediately after generating the args struct? There's a lot of stuff you're not freeing or deiniting 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.

Joystick initialization calls some USB ioctls that are not allowed by any pledge, so this is as early as the pledge call can go.

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 gave this another try.

src/platform/sdl/main.c Outdated Show resolved Hide resolved
@@ -264,6 +278,12 @@ int mSDLRun(struct mSDLRenderer* renderer, struct mArguments* args) {
state->close(state);
}
}
#ifdef USE_PLEDGE
if (!mPledgeNarrow(args)) {
Copy link
Member

Choose a reason for hiding this comment

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

What's the purpose of this second, narrower call? To drop further privs after everything is initialized? It looks like all you drop is dns and unix, and I'm not sure what those are used for in the first place. SDL I guess. Note that the runloop will still get called if this fails.

Also the indentation doesn't match.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that’s the purpose—pledge is intended to ratchet down permissions before entering the main loop.

dns and unix are needed for audio:

  • unix is needed when the device is a local sndiod(8) sub-device.
  • inet and dns are needed when the device is a remote sndiod(8) sub-device.

Once no further calls to sio_open() will be made, all these pledge(2) promises may be dropped, except for the audio promise.

Copy link
Contributor Author

@bentley bentley Jul 10, 2020

Choose a reason for hiding this comment

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

Is the right thing here to return didFail right away and let the caller deinit everything?

Edit:

Narrow pledge() failed
Segmentation fault (core dumped)

Apparently not.

Copy link
Member

Choose a reason for hiding this comment

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

Everything after the runloop except for mCoreThreadHasCrashed still needs to be called.

src/platform/sdl/main.c Outdated Show resolved Hide resolved
Copy link
Member

@endrift endrift left a comment

Choose a reason for hiding this comment

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

Pretty close this time. Just some minor stuff and one less minor thing.

@@ -264,6 +278,12 @@ int mSDLRun(struct mSDLRenderer* renderer, struct mArguments* args) {
state->close(state);
}
}
#ifdef USE_PLEDGE
if (!mPledgeNarrow(args)) {
Copy link
Member

Choose a reason for hiding this comment

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

Everything after the runloop except for mCoreThreadHasCrashed still needs to be called.

@@ -489,6 +496,10 @@ find_feature(USE_SQLITE3 "sqlite3")
find_feature(USE_ELF "libelf")
find_feature(ENABLE_PYTHON "PythonLibs")

if(USE_PLEDGE)
Copy link
Member

Choose a reason for hiding this comment

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

You forgot to delete it here.

renderer.core->deinit(renderer.core);
mSDLDeinitEvents(&renderer.events);
mSDLDeinit(&renderer);
fputs("Broad pledge() failed\n", stderr);
Copy link
Member

Choose a reason for hiding this comment

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

fputs already appends the \n.

mCoreConfigDeinit(&renderer.core->config);
mInputMapDeinit(&renderer.core->inputMap);
renderer.core->deinit(renderer.core);
mSDLDeinitEvents(&renderer.events);
Copy link
Member

Choose a reason for hiding this comment

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

mSDLDeinitEvents is called by mSDLDeinit, you don't need to call it yourself. mCoreConfigFreeOpts should be called though.

#ifdef USE_PLEDGE
if (!mPledgeNarrow(args)) {
didFail = true;
fputs("Narrow pledge() failed\n", stderr);
Copy link
Member

Choose a reason for hiding this comment

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

Ditto about the \n.

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.

2 participants