LiquidPlayer / LiquidCore

Node.js virtual machine for Android and iOS
MIT License
1.01k stars 127 forks source link

SECCOMP error while using Node on Android-O #78

Closed neopryn closed 5 years ago

neopryn commented 5 years ago

I've been implementing LiquidCore into Xamarin.Android as a way to execute JS in android and so far it's been pretty simple. While testing across a few different devices i've run into an issue with a core event method that seems to crash the app entirely on android version 26 and up.

The issue comes from using setTimeout and the resulting exception when its used is Fatal signal 31 (SIGSYS), code 1 (SYS_SECCOMP). After digging around a bit I've found its got something to do with a syscall filter introduced in API version 26 of android that blacklists syscalls that have been found to have security implications. Details can be found here: https://android-developers.googleblog.com/2017/07/seccomp-filter-in-android-o.html

I can't get full details on the sys_call that cause the failure but i've found some evidence that one of the dependencies for node 8.9.3 called libuv is responsible for making a particular blacklisted sys_call epoll_wait that triggers this https://github.com/libuv/libuv/issues/2105. I feel like there's multiple ways of getting around this however I wouldn't be surprised if this ends up being a problem in future with other restricted calls that node doesn't typically have problems with.

Is there strategy for dealing with this sort of this and is it possible to polyfill the behaviour of these methods (maybe by using native timers)?

ericwlange commented 5 years ago

Interesting. According to the issue you referenced, these direct syscalls were replaced in libuv 1.23.1. Node 8.9.3, which is the version used by LiquidCore uses v1.15.0. The latest 8.x LTS (8.15.0) uses v1.23.2, probably not coincidentally. It looks like it is time for me to upgrade node versions. I have been planning to upgrade to 11.x eventually, but a minor upgrade should be much simpler so I'll do that. Do you have a simple failing test I can use to make sure this is the right solution?

As for a workaround, you can certainly polyfill it with a native implementation. Untested, it would probably look something like:

private SparseArray<Thread> threadMap = new SparseArray<>();

// MicroService.ServiceOnStartListener --
@Override
public void onStart(MicroService service) {
    service.getProcess().addEventListener(new Process.EventListener() {
        @Override public void onProcessStart(Process process, final JSContext context) {
            context.property("setTimeout", new JSFunction(context, "setTimeout") {
                public Integer setTimeout(final JSFunction fn, final Integer ms, final JSValue ... args) {
                    final Thread thread = new Thread() {
                        @Override public void run() {
                            Thread.sleep(ms);
                            if (threadMap.get(thread.hashCode()) == thread) {
                                fn.apply(null, args);
                                threadMap.remove(thread.hashCode());
                            }
                        }
                    };
                    threadMap.put(thread.hashCode(), thread);
                    thread.start();
                    return thread.hashCode();
                }
            });

            // You would then have to implement setInterval and clearTimer similarly
        }
        @Override public void onProcessAboutToExit(Process process, int exitCode) {}
        @Override public void onProcessExit(Process process, int exitCode) {}
        @Override public void onProcessFailed(Process process, Exception error) {}
    });
}

The only challenge with this is that setTimeout() in node does some magic to keep the process alive while it is running. I don't think this polyfill will do that, although I am not entirely sure. You might also have to call process.keepAlive() explicitly in the above onStart() handler.

If you come up with a version that generally works, please post it here so others can use the workaround. I'll work on getting node 8.15.0 integrated.

neopryn commented 5 years ago

Sorry for the delay, I previously attempted to polyfill this using the event system provided by LiquidCore but couldn't find success due to the process dying before the callback could be completed. It seems Process.KeepAlive() also triggers the same problem (SECCOMP) so under the hood it must be triggering the same operations to keep the process from dying. From what I can see there's no way to halt the process without updating node at this point.

As for a reliable way to trigger this, all that should be required is a blank liquidserver project (with setInterval in the template) running in the node process on Android API 26 and up.

ericwlange commented 5 years ago

Are you seeing this on the emulator? Or is this on a specific piece of hardware?

Can you point me to an exact image I can use for my emulator to replicate this? I have tried API 28 x86_64, API 26 x86, and API 28 arm64 (actual Pixel running Android P) and I cannot replicate this bug with v0.6.0.

Also, can you tell me if any of the instrumentation tests fail? There are several tests that rely on keepAlive and setInterval() that should fail, but I am not able to get them to.

I would love to swat this, but I need to be able to see it fail first! Thanks.

neopryn commented 5 years ago

Hmm I'm starting to think these added security measures may be a red herring. I'm specifically testing x86_64 API 27 and API 28 at the moment in the emulator. Though this is through the JNI as i'm integrating this with Xamarin so it's a bound library. If all is well in native android then I can only assume it's got something to do with the JNI, it's just very strange that API 25 doesn't have the issue which indicates that they filters do probably play a roll.

Given that Xamarin isn't in scope with the project i'm happy to leave it be, I can give this a test once Node is updated and see if it makes a difference at all. Would be awesome to get this working at some point so i'll keep an eye on it.