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

[Feature]: Plugins without shared UID #2654

Open
tareksander opened this issue Mar 11, 2022 · 36 comments · May be fixed by #2921
Open

[Feature]: Plugins without shared UID #2654

tareksander opened this issue Mar 11, 2022 · 36 comments · May be fixed by #2921

Comments

@tareksander
Copy link
Member

Feature description

For security reasons (privilege separation) and because f-droid seems to be slow when it comes to setting up signing apps with the same key (see Termux:GUI) it could be useful to have a plugin that doesn't share the same UID. The main problem with that would be security: If the apps are different users, they cannot call not exported components of each other, and completely open components are a security risk.

My idea is: Create a new signature-level permission for Termux and have the plugins BroadcastReceiver or Service require that permission to get called. That way only Termux can call these components.

To establish a 2-way communication between a plugin and a program you could make Termux expose another unix socket to programs like for the am integration. The program sends Termux the component it would like to call and if successful Termux sends a file descriptor for a unix socket pair to the program and the other end is send in the Intent via ParcelFileDescriptor to the plugin. If more connections are needed, the program or plugin can then send more file descriptors over the connection they have already.

Additional information

Create a ParcelFileDescriptor socket pair.

Getting a FileDescriptor from a ParcelFileDescriptor.

Sending a FileDescriptor over a LocalSocket.

Putting a Parcelable into an Intent.

@agnostic-apollo
Copy link
Member

Yeah, signature level permission and am way can be useful. Not all plugins require direct access to termux files and sharedUserId. And sharedUserId may be phased out in some future release so we need to be prepared. Some plugins may need to be merged in termux-app if necessary, termux-boot and termux-styling are already going to be.

https://commonsware.com/blog/2022/02/12/random-musings-android-13-dp1.html#what-else-might-break-your-apps-in-the-not-too-distant-future

I was reviewing the termux-am integration yesterday, already rebased, but need to fix/change some things first before I can merge. Have to look more at the code too.

@tareksander
Copy link
Member Author

tareksander commented Mar 30, 2022

Can I start working on the plugin system from the am-integration branch or are there still changes that should be made there first?
Maybe we can get plugins without shared UID working before f-droid fixes the signature issue for Termux:GUI :) .

@agnostic-apollo
Copy link
Member

If you plan on working on it, I can try to work on the fixes/changes today. Need it for termux-api anyways. Apologies for the delays again.

@tareksander
Copy link
Member Author

I did a little bit more planning, let me know if you see any issues:

  • add a com.termux.app signature permission to Termux (signature permission, so no other app can even request it). This permission will be used by the plugins to identify the Termux app. Also there can't be 2 permissions with the same name, so the app can't be impersonated.
  • the com.termux.app permission is regarded as secure by the plugins. There are 2 scenarios where that could not be the case:
    • Termux isn't installed, so the permission name is free to be used by other apps. This isn't likely, because the plugins can't do anything without the main app. Also this would be immediately obvious as soon as you try to install Termux, as it would give an error, because the permission name is in use.
    • The user installed a malicious version of the com.termux package. In this case, the permission can't be trusted. A possible solution could be to include the valid signatures in the plugins and check that the app providing the permission is signed with one of the signatures, but this case is also not likely, and if you installed a trojan as Termux, you have other problems than plugins.
  • a new dangerous permission com.termux.plugin is added that the user has to grant for each plugin for it to be able to be called from Termux. This ensures that the user has to give permission for apps to become a Termux plugin.
  • a "plugin" unix filesystem socket is placed in the same directory as the am socket (wherever that ultimately ends up being). Programs can open this socket and write the plugin package name and then a null byte to the socket. Termux then sends an ordered Broadcast that can only be received if the app has the com.termux.plugin permission, with the file descriptor for the socket as a ParcelFileDescriptor. At the end of the ordered broadcast Termux closes its socket connection. If the broadcast wasn't received, this closes the only remaining connection and the program can tell the connection was closed, so there was no plugin with the right name + permission. If it was received, both the plugin and the program now have a 2 way communication channel to each other.
  • The Broadcast receiver in the plugins has to be exported, so it can be called by Termux. To keep it secure, the receiver has to specify android:permission="com.termux.app" so it can only be called by Termux.

When I implement the system I'll add tests to check if the system is secure, e.g. trying to connect to the plugin receiver from an external app, trying to call an app without the plugin permission, ... .

@tareksander
Copy link
Member Author

Another thing I'll look into is a dummy bound service that Termux can bind to, so plugins get a lower oom_adj score but don't have to make their own foreground services for that.

@agnostic-apollo
Copy link
Member

add a com.termux.app signature permission to Termux

Not necessary really. A function like TermuxUtils.isTermuxAppInstalled() could be added with an additional check for signature matching.

Moreover, permissions should start with com.termux.permission.*

a "plugin" unix filesystem socket is placed in the same directory as the am socket (wherever that ultimately ends up being). Programs can open this socket and write the plugin package name and then a null byte to the socket.

Any app that does not have a sharedUserId with termux will not be able to connect to filesystem sockets. The idea of additional package names/uid that should be allowed to connect will not work either, unless we use abstract socket if user sets a prop.

https://man7.org/linux/man-pages/man7/unix.7.html

On Linux, connecting to a stream socket object requires write permission on that socket; sending a datagram to a datagram socket likewise requires write permission on that socket.

Termux then sends an ordered Broadcast that can only be received if the app has the com.termux.plugin permission, with the file descriptor for the socket as a ParcelFileDescriptor.

Any app would be able to send the broadcast, you can't verify sender identity in BroadcastReceiver. Possibly uid checks could be done after socket connection.

The Broadcast receiver in the plugins has to be exported, so it can be called by Termux. To keep it secure, the receiver has to specify android:permission="com.termux.app" so it can only be called by Termux.

The sendOrderedBroadcast() receiverPermission would be com.termux.plugin so it can't also be com.termux.app.

https://developer.android.com/guide/topics/manifest/receiver-element#prmsn

Look into Shizuku, possibly binder AIDL APIs can be added in termux app that can be used by plugin apps. Would be much simpler probably and APIs itself would be nice, better interaction between apps, specially with custom java objects instead of just intents.

https://github.com/RikkaApps/Shizuku-API#using-shizuku-apisuserservice

Another thing I'll look into is a dummy bound service that Termux can bind to, so plugins get a lower oom_adj score but don't have to make their own foreground services for that.

Plugins should have their own foreground services, there are app specific restrictions even with sharedUserId. I plan on adding it for termux-api, currently for location api since its broken, it would be required anyways. And a user should know if an app is accessing location or other private hardware in background.

https://developer.android.com/about/versions/11/privacy/foreground-services

I also don't really like the idea of dummy bound services to force lower oom_adj.

p.s I got caught up on socket programming basics again, found some more issues, will start work on the changes today/tomorrow.

@tareksander
Copy link
Member Author

tareksander commented Apr 4, 2022

a "plugin" unix filesystem socket is placed in the same directory as the am socket (wherever that ultimately ends up being). Programs can open this socket and write the plugin package name and then a null byte to the socket.

Any app that does not have a sharedUserId with termux will not be able to connect to filesystem sockets. The idea of additional package names/uid that should be allowed to connect will not work either, unless we use abstract socket if user sets a prop.

The socket is for a program in Termux to connect to a plugin, not the other way around. The program runs as the Termux uid and can access the socket. If a plugin needs to connect by itself, RUN_COMMAND intents can be used.

Any app would be able to send the broadcast, you can't verify sender identity in BroadcastReceiver.

That's what the signature permission is for, to make the android system able to do that check. Quoting the Android docs:

You can also use a permission to limit the external entities that can send it messages (see the permission attribute).

android:permission
The name of a permission that broadcasters must have to send a message to the broadcast receiver. If this attribute is not set, the permission set by the element's permission attribute applies to the broadcast receiver. If neither attribute is set, the receiver is not protected by a permission.

The sendOrderedBroadcast() receiverPermission would be com.termux.plugin so it can't also be com.termux.app.

That specifies the permission the receiving app must have to receive the broadcast. com.termux.app is the permission Termux has itself and doesn't need to be declared. Again quoting the Android docs:

receiverPermission
String: String naming a permissions that a receiver must hold in order to receive your broadcast. If null, no permission is required.

Look into Shizuku, possibly binder AIDL APIs can be added in termux app that can be used by plugin apps. Would be much simpler probably and APIs itself would be nice, better interaction between apps, specially with custom java objects instead of just intents.

Binder is nice to use from an app perspective, but binder support is not included in the NDK and is unavailable to the programms running in Termux. Using services instead of a broadcast to connect to plugins is also possible though.

@agnostic-apollo
Copy link
Member

The socket is for a program in Termux to connect to a plugin, not the other way around. The program runs as the Termux uid and can access the socket. If a plugin needs to connect by itself, RUN_COMMAND intents can be used.

Ah, sorry, termux-api was the only plugin that gets called from termux-app, all others call termux-app, so I was going that way. But now there is termux-gui and another yet to be released that does get called from termux-app, so by programs you meant native commands and not plugins.

We can use dedicated Service instead of BroadcastReceiver with permission protection, external plugins can directly connect to it. Can validate identify as well if needed to ensure non plugin apps don't use specific APIs. Current RUN_COMMAND execution intent would slow things down. First RunCommandService will receive, then TermuxService, then new process started, then BroadcastReceiver sent a message. Additional actions are planned for RunCommandService too, so can actually just use it directly too, instead of an execution intent.

That's what the signature permission is for, to make the android system able to do that check. Quoting the Android docs:

That specifies the permission the receiving app must have to receive the broadcast. com.termux.app is the permission Termux has itself and doesn't need to be declared. Again quoting the Android docs:

The Context#sendOrderedBroadcast() stated

receiverPermission: String naming a permissions that a receiver must hold in order to receive your broadcast. If null, no permission is required.

I assumed that by receiver it meant the receiver component android:permission, it is actually receiver app as you correctly said. I confirmed this with tests and checked AOSP. They should make it clear in the docs. One can technically use same permission as param and in target receiver component to ensure permission is not changed in some new version.

Basically, your proposal is that com.termux.permission.plugin dangerous permission is passed as sendOrderedBroadcast() param and com.termux.permission.app signature permission set as android:permission in component. Additionally, the full component name must be set for the plugin app in the ordered intent sent so that it is not sent to some other app. The "program" should pass both package and class name, if different components are to be sent an intent belonging to the same app.

https://cs.android.com/android/platform/superproject/+/android-11.0.0_r48:frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java;l=655

But I am not satisfied with just signature based permissions, the plugins should ensure that command came from com.termux package itself. Our github releases private key is public, so malicious apps could just use that, this would specially be important if sharedUserId goes away. The calling package check can't be done with just a BroadcastReceiver or Service intent, unless we bind to the service and call Binder.getCallingUid(). However, if termux-app generates a PendingIntent and sends it in the ordered broadcast, the plugin can call PendingIntent.getCreatorPackage() to verify its indeed com.termux calling it since it can't be spoofed. The pending intent could even be used to send something back to termux-app, like a result or just send back as proof of successful connection. This would provided better security.

The pending intent should have PendingIntent.FLAG_ONE_SHOT and unique request code to prevent overrides, like termux-tasker does.

https://github.com/termux/termux-tasker/blob/v0.6.0/app/src/main/java/com/termux/tasker/utils/PluginUtils.java#L188

And we necessarily don't need to send to a BroadcastReceiver with sendOrderedBroadcast(), one can verify if plugin has com.termux.permission.plugin dangerous permission with a call to PackageManager.checkPermission(), like termux-tasker does to check if plugin host app like Tasker has RUN_COMMAND permission in config activity to warn users. So sending intents to services should be possible too, actually preferred, since broadcast receivers can only run commands for few seconds. If using targetSdkVersion 30+, then will need to add plugin names to queries element or declare QUERY_ALL_PACKAGES permission in AndroidManifest.xml, otherwise will always get false, check here.

https://github.com/termux/termux-tasker/blob/v0.6.0/app/src/main/java/com/termux/tasker/utils/PluginUtils.java#L471

Termux sends a file descriptor for a unix socket pair to the program and the other end is send in the Intent via ParcelFileDescriptor to the plugin

That's not gonna work. You can't send ParcelFileDescriptor in intents. And even if you could, the fd for the AF_UNIX/SOCK_STREAM socket created with ParcelFileDescriptor.createSocketPair() is created in /proc/$TERMUX_APP_PID/fd, which the plugin app won't have read/write access to normally.

https://stackoverflow.com/questions/18706062/android-exception-with-sending-parcelfiledescriptor-via-intent

https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/jni/android_util_Binder.cpp;l=898

https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/native/libs/binder/Parcel.cpp;l=1322

try {
    ParcelFileDescriptor[] pfd = ParcelFileDescriptor.createSocketPair();
    Logger.logInfo(LOG_TAG, "socket: " + pfd[0].getFd());
    Logger.logInfo(LOG_TAG, "socket: " + pfd[1].getFd());
    Intent serviceIntent = new Intent(this, TermuxService.class);
    serviceIntent.putExtra("fd", pfd[1]);
    startService(serviceIntent);
} catch (Exception e) {
    e.printStackTrace();
}
W/System.err: java.lang.RuntimeException: Not allowed to write file descriptors here
W/System.err:     at android.os.Parcel.nativeWriteFileDescriptor(Native Method)
W/System.err:     at android.os.Parcel.writeFileDescriptor(Parcel.java:856)
W/System.err:     at android.os.ParcelFileDescriptor.writeToParcel(ParcelFileDescriptor.java:1092)
W/System.err:     at android.os.Parcel.writeParcelable(Parcel.java:1904)
W/System.err:     at android.os.Parcel.writeValue(Parcel.java:1810)
W/System.err:     at android.os.Parcel.writeArrayMapInternal(Parcel.java:975)
W/System.err:     at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1620)
W/System.err:     at android.os.Bundle.writeToParcel(Bundle.java:1303)
W/System.err:     at android.os.Parcel.writeBundle(Parcel.java:1044)
W/System.err:     at android.content.Intent.writeToParcel(Intent.java:10855)
W/System.err:     at android.app.IActivityManager$Stub$Proxy.startService(IActivityManager.java:5827)
W/System.err:     at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1700)
W/System.err:     at android.app.ContextImpl.startService(ContextImpl.java:1670)
W/System.err:     at android.content.ContextWrapper.startService(ContextWrapper.java:720)
W/System.err:     at android.content.ContextWrapper.startService(ContextWrapper.java:720)
W/System.err:     at com.termux.app.TermuxActivity.lambda$setSettingsButtonView$2$TermuxActivity(TermuxActivity.java:576)
W/System.err:     at com.termux.app.-$$Lambda$TermuxActivity$F_s74DJTj5Xgzs9mYOYZfwC1C1s.onClick(Unknown Source:2)
W/System.err:     at android.view.View.performClick(View.java:7448)
W/System.err:     at android.view.View.performClickInternal(View.java:7425)
W/System.err:     at android.view.View.access$3600(View.java:810)
W/System.err:     at android.view.View$PerformClick.run(View.java:28305)
W/System.err:     at android.os.Handler.handleCallback(Handler.java:938)
W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:99)
W/System.err:     at android.os.Looper.loop(Looper.java:223)
W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:7664)
W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
W/System.err:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

I also forgot that abstract unix sockets across apps won't work if using targetSdkVersion 28 to due sepolicy restrictions. The idea for whitelisted package name/uids will not work either and only termux or root will be able to access am socket on Android 9+.

https://developer.android.com/about/versions/pie/android-9.0-changes-28

android/ndk#1469

https://stackoverflow.com/questions/63806516/avc-denied-connectto-when-using-uds-on-android-10

Since the above won't work, the solution that was suggested by @twaik here was using termux ContentProvider to return socket ParcelFileDescriptor if the mode passed to openFile had a special string.

So how this would work would be that when native process calls am to call the plugin, a unique token for the process is passed in the intent as well and a PendingIntent and received by plugin service, then plugin calls termux ContentProvider with socket_pair_creation:<intent> command and also passes the intent it received from termux app. The ContentProvider receives the command and creates the socket pair, and then returns the second fd to plugin for it to read the command from the program. The fist socket is just added to some static hash map with the token:type key, currently termux app just writes the command as a POC. We should probably design a better api that the simple socket_pair_creation command, since if multiple sockets need to be created, the plugin can call multiple times, with something like stdout and stderr or what the native program passes in addition to component name. Maybe a space separated list of socket types. The token exists so that we return socket fd to correct program.

When the plugin has successfully created all sockets pairs, then it calls PendingIntent.send(), which is received by termux app and it then returns all the socket fds to native program and communication can commence. As to how to return socket fds to program will require some design and synchronization.

May need to call F_SETFL to remove O_NONBLOCK flag as well.

This is currently working on Android 11 with plugin using targetSdkVersion 30. The content provider is protected by RUN_COMMAND permission as well so should be safe.

https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:cts/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java;l=180

https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/services/Car/service/src/com/android/car/pm/CarPackageManagerService.java;l=1439

// app/src/main/java/com/termux/app/TermuxOpenReceiver$ContentProvider


@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
    if (mode.startsWith("socket_pair_creation:")) {
        Intent intent = null;
        try {
            intent = Intent.parseUri(mode.replaceAll("^socket_pair_creation:", ""), 0);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        if (intent == null) {
            return null;
        }

        Log.d(LOG_TAG, "intent: " + intent);

        ParcelFileDescriptor[] fd;
        try {
            fd = ParcelFileDescriptor.createSocketPair();
            
            // Just add to some static hash map, this is just a sample
            writeSocketCommand(fd[0]);
        } catch (IOException e) {
            throw new FileNotFoundException(e.getMessage());
        }
        return fd[1];
    }

   ...
}


private void writeSocketCommand(ParcelFileDescriptor parcelFileDescriptor) {
    new Thread() {
        @Override
        public void run() {
            FileOutputStream fileOutputStream = null;
            BufferedWriter bufferedWriter = null;
            try {
                Log.d(LOG_TAG, "parcelFileDescriptor: " + parcelFileDescriptor.describeContents());
                fileOutputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
                bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream, Charset.defaultCharset()));
                bufferedWriter.write("echo 'some data'\n");
                bufferedWriter.flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (parcelFileDescriptor != null)
                        parcelFileDescriptor.close();
                    if (fileOutputStream != null)
                        fileOutputStream.close();
                    if (bufferedWriter != null)
                        bufferedWriter.close();

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }.start();
}
// plugin service


@Override
public int onStartCommand(Intent intent, int flags, int startId) {

	new Thread() {
        @Override
        public void run() {

        readSocketCommand(this, intent);
        }

    }.start();
}

private void readSocketCommand(Context context, Intent intent) {
    // Dummy path to call com.termux.files content provider
    Uri uri = Uri.parse("content://com.termux.files/data/data/com.termux/files/usr");
    ParcelFileDescriptor parcelFileDescriptor = null;
    try {
        Log.i(LOG_TAG, "socket_pair_creation");
        parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "socket_pair_creation:" + intent.toUri(0));
        Log.d(LOG_TAG, "parcelFileDescriptor: " + parcelFileDescriptor.describeContents());
        String command = readFullStreamOrError(
                new FileInputStream(parcelFileDescriptor.getFileDescriptor()));
        Log.i(LOG_TAG, "command: " + command);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (parcelFileDescriptor != null)
                parcelFileDescriptor.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

private static String readFullStreamOrError(InputStream inputStream) {
    try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
        try {
            final byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                result.write(buffer, 0, length);
            }
        } catch (IOException e) {
            return result.toString("UTF-8") + " exception [" + e + "]";
        }
        return result.toString("UTF-8");
    } catch (IOException e) {
        return e.toString();
    } finally {
        try {
            if (inputStream != null)
                inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Another way could be to send intents to plugin with FLAG_GRANT_WRITE_URI_PERMISSION with a path to a unique file in sockets directory in intent data uri. It then starts a filesystem unix socket, since it should have write permissions to the file, even if under termux files directory. I haven't tested this, but it may work, but will probably require multiple intents to plugin app for each socket type, so would be slow.

Binder is nice to use from an app perspective, but binder support is not included in the NDK and is unavailable to the programms running in Termux. Using services instead of a broadcast to connect to plugins is also possible though.

Well, there is NdkBinder added in API level 29, but we can't really use it currently, since my daily device is still on Android 7 :p

https://developer.android.com/ndk/reference/group/ndk-binder

https://android.googlesource.com/platform/frameworks/native/+/master/libs/binder/ndk/tests

https://github.com/lakinduboteju/AndroidNdkBinderExamples

For your use case, not using AIDL APIs may suffice probably, what protocol are you thinking?

For plugin java stuff, can just bind to services and use AIDL APIs.

@twaik
Copy link
Member

twaik commented Apr 7, 2022

About that. I have no much time but I have already ported jekstrand's sources which will allow us to use Wayland in termux:app. But wayland is asynchronous protocol so we can not receive returned result to the same thread we sent a request and I am not sure what I should do with this.
Should I create a new server connection for every thread and keep it with pthread_get_specific? It is problematic in case where I will use an object created in another thread.
Should I implement transactions mechanism? It will be harder than reimplement libwayland with keeping marshalling and dispatching mechanisms.
Maybe it is possible to port android-7.0's Binder to termux? It will be problematic in case where we will try to port it to ubuntu/arch/smth to be used with proot.

You can try to play with sources I have ported. But there is no protocol I have implemented.
termux-api.tar.gz

@tareksander
Copy link
Member Author

That's not gonna work. You can't send ParcelFileDescriptor in intents.

Usually the Android docs are really good, but it seems that fact wasn't put in.

And we necessarily don't need to send to a BroadcastReceiver with sendOrderedBroadcast(), one can verify if plugin has com.termux.permission.plugin dangerous permission with a call to PackageManager.checkPermission(), like termux-tasker does to check if plugin host app like Tasker has RUN_COMMAND permission in config activity to warn users. So sending intents to services should be possible too, actually preferred, since broadcast receivers can only run commands for few seconds. If using targetSdkVersion 30+, then will need to add plugin names to queries element or declare QUERY_ALL_PACKAGES permission in AndroidManifest.xml, otherwise will always get false, check here.

Yeah, a service is probably better.

Well, there is NdkBinder added in API level 29, but we can't really use it currently, since my daily device is still on Android 7 :p

https://developer.android.com/ndk/reference/group/ndk-binder

https://android.googlesource.com/platform/frameworks/native/+/master/libs/binder/ndk/tests

We should have a system that works on all API versions supported by Termux, so NdkBinder isn't usable.
The binder unit test looks interesting though. I tried to find a way to use an android service only using the NDK, but all solutions I found called the Java implementation.

This is currently working on Android 11 with plugin using targetSdkVersion 30. The content provider is protected by RUN_COMMAND permission as well so should be safe.

I would also like to have something like lower-privilege plugins that don't need RUN_COMMAND.

We should probably design a better api that the simple socket_pair_creation command, since if multiple sockets need to be created, the plugin can call multiple times, with something like stdout and stderr or what the native program passes in addition to component name. Maybe a space separated list of socket types. The token exists so that we return socket fd to correct program.

When the plugin has successfully created all sockets pairs, then it calls PendingIntent.send(), which is received by termux app and it then returns all the socket fds to native program and communication can commence. As to how to return socket fds to program will require some design and synchronization.

Having the ability to create multiple socket connections would be nice, but can always be made manually by creating a socket pair and sending one of the fds through the existing socket.

My new idea:
A program in Termux wants to connect to a plugin:

  • the program opens the Termux's plugin socket and sends a byte indicating how many socket connections it wants (255 should be enough for anything), then the component name of the service in the plugin, then a null byte.
  • Termux creates the specified amount of socket pairs, and creates an IBinder implementation with them (probably the easiest using a Messenger). The Binder is then put in a Bundle, which is used as an intent extra. An empty PendingIntent is also put as an extra. Then the intent is used to start the service in the plugin.
  • the plugin then has a short time (~2s, idk) to verify the Termux package name from the PendingIntent and get the socket files over the Binder.
  • if that timeout expires a non-zero byte is send to the program, followed by an error message, and the Binder is discarded.
  • as soon as the plugin gets the sockets, Termux also sends a zero byte to the program with all of the other socket file descriptors as ancillary data.

After this connection, the initial socket connection from the program to Termux could be closed, or you could keep it open in case it's needed in future improvements. The same for the Binder, maybe new calls could be added in the future. This method doesn't need the RUN_COMMAND permission and doesn't need to abuse the content provider. Also there are no tokens that could potentially be brute-forced.

A plugin wants to connect to Termux:
I don't know what exactly this could accomplish, but having the option to do it this way could be good in the future. The only reason I can think of is that maybe using Binder IPC is faster than using a RUN_COMMAND intent.
For that we can use a bound service in Termux, protected by the new plugin permission. For methods that run commands, the RUN_COMMAND permission can be checked for the calling app at runtime. This Binder should probably use AIDL.

Sending fds over a socket needs some additions to LocalFilesystemSocket, so I'll wait with a prototype until the changes you made are merged first.

@twaik
Copy link
Member

twaik commented Apr 7, 2022

That's not gonna work. You can't send ParcelFileDescriptor in intents.

I already achieved this in Lorie. It is true that it is impossible to send fd using intent, but we can send IBinder. This IBinder will be connected to interface (aidl or raw binder) which requests needed file descriptor. One or many.
But anyway I think that using ContentProvider will be more correct.

@twaik
Copy link
Member

twaik commented Apr 7, 2022

Looks like there are no options.

  1. Wayland is good. It supports Unix socket, file descriptor exchange, resource management and code generation. There is some unofficial Java binding (including code generation). But it is asynchronous, I did not found any way to return answer value to the thread a request was sent from.
  2. gRPC. Has code generation and resource management, has official Java and C++ support. But has no Unix socket support and can not send file descriptors.
  3. Microsoft Thrifty. The same thing.
  4. NXP eRPC. Has code generation, synchronous, but have no Java support, can not send file descriptors and resource management.
  5. Android Binder (android 7 ported to termux). We can use old sources. It can work. But we will need to embed all the sources to termux codebase which is not good.

Is there anyone who has any idea how to handle this?

@tareksander
Copy link
Member Author

@agnostic-apollo I finished a first version of the plugin system. It's in my Termux fork.
The principle:
The new permission com.termux.permission.TERMUX_SIGNATURE as a signature permission that only Termux can hold. This is used so exported components of plugins aren't fully exposed, but it should always be verified if the sender is indeed Termux and the app signature is correct.
The new permission com.termux.permission.TERMUX_PLUGIN as a dangerous permission that has to be requested by plugins to be able to bind to the plugin service inside Termux.

Plugin directories: Each plugin gets its own directory under /data/data/com.termux/files/apps/plugins/ with the package name as the name of the directory.

The plugin service: A bound service in Termux that is protected by the new TERMUX_PLUGIN permission. The interface to the service is defined in AIDL in a new module that can be published separately on Jitpack, so plugins don't have to include all the Termux libraries. After connecting a plugin has to provide the service with a Binder that can be used for callbacks: Either provide a Binder directly or specify a component name for a service to bind to. The binder is also used to listen for the process death of the plugin to discard any state for the plugin in the service.

I also added Utilities to termux-shared for Binder communication to e.g. get the caller package name, which won't work for shared uid plugins without a wrapper. That can be useful for transitioning existing plugins to the new system while remaining compatible for a while.

I also made a new plugin-template repo that will contain a library for plugins and a template plugin to expand from for your own plugin. The library contains utilities to verify the Termux app signature and constants meaningful for plugins copied from TermuxConstants. A better solution would be to make termux-shared not depend on terminal-view and terminal-emulator and include the code there, but as it is now that is to much useless bloat, as a plugin can't make use of the terminal view or the terminal emulator itself. Right now you have to export the plugin-aidl module from Termux to a local maven repo to compile.

The current AIDL methods are:

  • setCallbackBinder() and setCallbackService(): Set the callback binder to initialize the connection.
  • runTask(): Run a command as a Termux task. This method additionally enforces that the plugin has the RUN_COMMAND permission.
  • listenOnSocketFile(): Creates a socket file with the specified name in the plugin directory and returns the file descriptor to the plugin.
  • openFile(): Opens a file with the specified name in the plugin directory with the specified mode and returns the file descriptor to the plugin.

For runTask() I'd need your help: This method should be better alternative to the RUN_COMMAND intent, because it should return the stdout and stderr file descriptors and accept a file descriptor for stdin. That way no input or output has to be truncated, because the plugin can read and write it itself. It would be more efficient to pass the file descriptors directly with dup'ing and no CLOEXEC flag than using the stream gobblers that are used now for tasks, so you don't have unnecessary threads and copying. I don't know enough about the existing codebase to integrate that into TermuxService myself, and it would probably also need a new native method to pass the file descriptors.

For listenOnSocketFile() I created a new method that creates a LocalServerSocket, but doesn't start it, so you can take the file descriptor and send it to the plugin. I don't know if there is a cleaner way to do this.

There are still some design decisions left:

  • Should there be a method to list the files and folders in the plugin directory? This could be used to spy out files and read them if the path verification that limits access to the plugin directory doesn't work. Or should advanced filesystem operations that could have such vulnerabilities be protected with another permission?
  • Should there be a method that acts more like the traditional RUN_COMMAND intent and returns the result via a method in the callback Binder instead of a PendingIntent? Or is the intent enough?
  • I would also like to have a way for Termux to bind to a plugin, because that way the plugin doesn't have to have it's own foreground service that clutters the notifications. There is no use for e.g. Termux:API to create a foreground service to process a request that takes 1s. Also the lifetime of the plugin would be bound to Termux, also good for things like Termux:API that exist just to be used by Termux and have no use otherwise.
  • Any ideas for additional methods?

@twaik
Copy link
Member

twaik commented May 9, 2022

I have some notes about using Java in C/C++ apps. termux/termux-packages#9100 (reply in thread)
I hope it will work.

@agnostic-apollo
Copy link
Member

I will get back to you guys after the bootstrap scripts fixes/updates. Again, apologies for the delays.

@625478A
Copy link

625478A commented May 10, 2022 via email

@tareksander
Copy link
Member Author

tareksander commented May 27, 2022

@agnostic-apollo I fixed listenOnSocketFile() and changed is so it calls a callback in the plugin with each connected client instead of returning the server socket. Apparently listening on the returned server socket didn't work, so I changed it. I also added tests for listenOnSocketFile() to the template repo.

Do you have time to look at it now?
For runTask() the reason why I can't implement it myself is that you would have to store the ParcelFileDescriptors for stdin, stdout and stderr in the Task until it is started, but they have to always be closed so you don't leak them. I don't know all the ways a Task can complete or error out, so I don't feel confident implementing that correctly.

Also: Are the FIleUtils methods and other parts of termux-shared thread safe? Binder transactions are processed from other threads and if the methods aren't thread safe, I'd have to schedule that on the main thread instead.

@twaik
Copy link
Member

twaik commented Jun 10, 2022

Hi guys.
After updating my phone to Android 12 termux-x11 fails to send activity intents and even when I am sending broadcast intents I am failig to retrieve ParcelFileDescriptor's (I am sending it to Binder on termux-app side but not able to read it on termux-x11 side).
Listening on socket files in @tareksander's version of termux-app looks very interesting so it can be used in my app.
Is there any chance to merge this?
@agnostic-apollo

@agnostic-apollo
Copy link
Member

Some changes are required and I need to look more into it. I am working on other higher priority stuff for next release, will get to it soon "hopefully".

@twaik
Copy link
Member

twaik commented Jun 10, 2022

Maybe I can help with it?

@agnostic-apollo
Copy link
Member

How about you review the code from a security perspective as well and also test the APIs and socket listening, etc, specially for your use case and on Android 12/13 and with targetSdkVersion 30+. I'll have to do the same as well. There are tonne of things on the to-do list that are far older that were requested or are higher priority, this is just one of them.

@twaik
Copy link
Member

twaik commented Jun 10, 2022

I am not good enough to review something from a security perspective... But can test something if you need.

@agnostic-apollo
Copy link
Member

It's fine, I'll do it.

@tareksander
Copy link
Member Author

I also tried to implement the runTask API myself, but somehow the started process is killed with a segfault. It's in the plugin-system-nativeshell branch.
For security: I already implemented some tests in the plugin template repository. The permission security system works. What I haven't tested yet is that a plugin refuses to connect to a com.termux package when the signature isn't recognized.

@agnostic-apollo
Copy link
Member

For runTask() the reason why I can't implement it myself is that you would have to store the ParcelFileDescriptors for stdin, stdout and stderr in the Task until it is started, but they have to always be closed so you don't leak them. I don't know all the ways a Task can complete or error out, so I don't feel confident implementing that correctly.

If you are talking about AppShell, it has synchronous mode so that it will always return directly. In asynchronous mode it starts a thread and calls AppShellClient.onAppShellExited() on exit, it may return directly with an error too. So either of two ways for task to return. But if you start a thread, then obv, caller will have to be notified some other way.

If talking about the NativeShell, that will have to be looked into.

Are the FIleUtils methods and other parts of termux-shared thread safe?

No, they are not, other than I guess MoreFiles.deleteRecursively(). Use synchronized java method modifier where appropriate I guess.

I also tried to implement the runTask API myself, but somehow the started process is killed with a segfault. It's in the plugin-system-nativeshell branch.

I'll take a look. You can too, check https://source.android.com/devices/tech/debug and https://source.android.com/devices/tech/debug/native-crash

If exception is in termux-shared native lib, then run something like adb logcat | ndk-stack -sym termux-app/termux-shared/build/intermediates/ndkBuild/debug/obj/local/arm64-v8a > stack.txt to generate stacktrace from dump logged in logcat.

@tareksander
Copy link
Member Author

tareksander commented Jun 18, 2022

I fixed NativeShell, the error came from coreutils not knowing what to do when argv[0] is not present. I added a notice for runTask() for that. NativeShell also now has normal Termux environment variables, and there is a callback for finished Tasks and a method to send a signal to a started Task (plugins can only send signals to their own Tasks).
I looked at the FileUtils methods I used and they all only use local variables and parameters, so I guess they should be thread-safe (getCanonicalPath and isPathInDirPath).
I also merged the plugin-system-nativeshell into the plugin-system branch of my fork.

@agnostic-apollo
Copy link
Member

I fixed NativeShell

Great.

so I guess they should be thread-safe (getCanonicalPath and isPathInDirPath).

The functions itself don't use external variables, so should be safe, I meant you were asking thread safe from a filesystem perspective.

I also merged the plugin-system-nativeshell into the plugin-system branch of my fork.

Cool, will take a look. Your turn is almost here now I think, after I mainly write some docs. ;)

@twaik
Copy link
Member

twaik commented Jul 2, 2022

So what is happening? Can it be merged or it still should be reviewed?

@agnostic-apollo
Copy link
Member

Likely some time next week. Finishing up docs.

@twaik
Copy link
Member

twaik commented Jul 31, 2022

@agnostic-apollo
Can you please make a few changes to termux-shared?
It would be cool to have some function which will tell us if allow-external-apps is enabled and/or read termux.properties without allow-external-apps enabled. Also it would be good to make com.termux.shared.termux.settings.properties.TermuxSharedProperties load properties from com.termux using ContentResolver in the case if file is not available directly.
Thank you very much.

@agnostic-apollo
Copy link
Member

agnostic-apollo commented Aug 1, 2022

Call TermuxAppSharedProperties.init() and check for null. Also note that updating allow-external-apps value requires termux-reload-settings to be run or termux app to be force stopped. Termux content provider does not allow read/write without allow-external-apps to be set to true due to security/privacy reasons.

import android.content.Context;
import android.net.Uri;

import androidx.annotation.NonNull;

import com.termux.shared.android.PermissionUtils;
import com.termux.shared.errors.Error;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.net.uri.UriUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import com.termux.shared.termux.settings.properties.TermuxSharedProperties;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections;

public class TermuxAppSharedProperties extends TermuxSharedProperties {

    public static final String LOG_TAG = "TermuxAppSharedProperties";

    private static TermuxAppSharedProperties properties;


    private TermuxAppSharedProperties(@NonNull Context context, @NonNull String termuxPropertiesDestPath) {
        super(context, TermuxConstants.TERMUX_APP_NAME,
                Collections.singletonList(termuxPropertiesDestPath), TermuxPropertyConstants.TERMUX_APP_PROPERTIES_LIST,
                new TermuxSharedProperties.SharedPropertiesParserClient());
    }

    /**
     * Initialize the {@link #properties} and load properties from disk.
     *
     * @param context The {@link Context} for operations.
     * @return Returns the {@link TermuxAppSharedProperties}.
     */
    public static TermuxAppSharedProperties init(@NonNull Context context) {
        properties = null;

        if (!PermissionUtils.checkPermission(context, TermuxConstants.PERMISSION_RUN_COMMAND)) {
            Logger.logError(LOG_TAG, "TermuxAppSharedProperties.init() failed because \"" + TermuxConstants.PERMISSION_RUN_COMMAND + "\" permission has not been granted");
            return null;
        }

        if (!isAllowExternalAppsEnabled(context)) {
            Logger.logError(LOG_TAG, "TermuxAppSharedProperties.init() failed because \"" + TermuxConstants.PROP_ALLOW_EXTERNAL_APPS + "\" permission is not enabled");
            return null;
        }

        String termuxPropertiesDestPath = context.getFilesDir() + "/" + "termux.properties";

        for (String termuxPropertiesSrcPath : TermuxConstants.TERMUX_PROPERTIES_FILE_PATHS_LIST) {
            if (copyTermuxAppSharedPropertiesToOwnApp(context, termuxPropertiesSrcPath, termuxPropertiesDestPath)) {
                Logger.logInfo(LOG_TAG, "Successfully copied termux.properties from \"" + termuxPropertiesSrcPath + "\" to own app at \"" + termuxPropertiesDestPath + "\"");
                properties = new TermuxAppSharedProperties(context, termuxPropertiesDestPath);
                break;
            }
        }

        if (properties == null) {
            Logger.logError(LOG_TAG, "Failed to copy termux.properties to own app from: " + TermuxConstants.TERMUX_PROPERTIES_FILE_PATHS_LIST);
        }

        return properties;
    }

    /**
     * Get the {@link #properties}.
     *
     * @return Returns the {@link TermuxAppSharedProperties}.
     */
    public static TermuxAppSharedProperties getProperties() {
        return properties;
    }


    public static boolean copyTermuxAppSharedPropertiesToOwnApp(@NonNull Context context,
                                                                @NonNull String termuxPropertiesSrcPath,
                                                                @NonNull String termuxPropertiesDestPath) {
        InputStream inputStream = null;
        FileOutputStream fileOutputStream = null;

        try {
            Uri termuxPropertiesSrcUri = UriUtils.getContentUri(TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY, termuxPropertiesSrcPath);
            inputStream = context.getContentResolver().openInputStream(termuxPropertiesSrcUri);
            Error error = FileUtils.createParentDirectoryFile("termux.properties parent dir", termuxPropertiesDestPath);
            if (error != null) {
                Logger.logError(LOG_TAG, error.getErrorLogString());
                return false;
            }
            File outFile = new File(termuxPropertiesDestPath);
            fileOutputStream = new FileOutputStream(outFile);
            byte[] buffer = new byte[4096];
            int readBytes;
            while ((readBytes = inputStream.read(buffer)) > 0) {
                //Logger.logInfo(LOG_TAG, "data: " + new String(buffer, 0, readBytes, Charset.defaultCharset()));
                fileOutputStream.write(buffer, 0, readBytes);
            }
        } catch (Exception e) {
            Logger.logError(LOG_TAG, "Failed to copy termux.properties to own app: " + e.getMessage());
            return false;
        } finally {
            FileUtils.closeCloseable(inputStream);
            FileUtils.closeCloseable(fileOutputStream);
        }

        return true;
    }

    public static boolean isAllowExternalAppsEnabled(@NonNull Context context) {
        InputStream inputStream = null;

        try {
            // It wouldn't matter if login script exists, since allow-external-apps check is made earlier in TermuxOpenReceiver$ContentProvider
            Uri loginScriptPath = UriUtils.getContentUri(TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY,
                    TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login");
            inputStream = context.getContentResolver().openInputStream(loginScriptPath);
        } catch (Exception e) {
            String message = e.getMessage();
           if (message != null && message.contains("requires `allow-external-apps` property to be set to `true`"))
               return false;
        } finally {
            FileUtils.closeCloseable(inputStream);
        }

        return true;
    }

}

@twaik
Copy link
Member

twaik commented Aug 1, 2022

@agnostic-apollo
I know about copying termux.properties to my own app directories but can you please make it open directly through ContentResolver if it can not be opened through File?
java.util.Properties.load() in com.termux.shared.settings.properties.SharedProperties.getPropertiesFromFile() requires java.io.Reader as an argument and we can open it like

Uri termuxPropertiesSrcUri = UriUtils.getContentUri(TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY, termuxPropertiesSrcPath);
ContentResolver resolver = mContext.getContentResolver();
InputStreamReader reader = new InputStreamReader(resolver.openInputStream(termuxPropertiesSrcUri));
property.load(reader);

You can add something like this code to getPropertiesFromFile for the case where termuxPropertiesSrcPath is unaccessible (but maybe exists). Of course it will affect loadTermuxPropertiesFromDisk.loadTermuxPropertiesFromDisk() but it will let us use TermuxAppSharedProperties directly without extending existing classes.

@agnostic-apollo
Copy link
Member

Yeah, I didn't do it the way you are suggesting cause would have required modifying SharedProperties and TermuxSharedProperties, but I guess, Uri support could be a good feature to add. But SharedProperties.getPropertiesFromFile() won't be messing around with RUN_COMMAND permission or allow-external-apps, it will just log exception/show toast. Will have to check for those before loading properties. Also TermuxPropertyConstants.TERMUX_APP_PROPERTIES_LIST would have to converted to Uris and checked which is available as in previous comment like SharedProperties.getPropertiesFileFromList() does in loadTermuxPropertiesFromDisk(). Can check if mContext has same sharedUserId as termux app and handle accordingly. Will look into it.

@twaik
Copy link
Member

twaik commented Aug 1, 2022

Thank you.

@tareksander tareksander linked a pull request Aug 5, 2022 that will close this issue
@twaik
Copy link
Member

twaik commented Mar 9, 2023

Actually I found a bit simpler way to make Termux plugin without making lots of changes to termux-app codebase. It is possible to simply send command to termux to run .apk of application that sends it like this.

/**
 * https://stackoverflow.com/questions/16973248/how-can-i-get-the-apk-file-name-and-path-programmatically
 * Get the apk path of this application.
 * @param context any context (e.g. an Activity or a Service)
 * @return full apk file path, or null if an exception happened (it should not happen)
 */
public static String getApkName(Context context) {
    String packageName = context.getPackageName();
    PackageManager pm = context.getPackageManager();
    try {
        ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
        String apk = ai.publicSourceDir;
        return apk;
    } catch (Throwable ignored) {}
    return null;
}

...

Intent intent = new Intent();
intent.setClassName("com.termux", "com.termux.app.RunCommandService");
intent.setAction("com.termux.RUN_COMMAND");
intent.putExtra("com.termux.RUN_COMMAND_PATH", "env");
intent.putExtra("com.termux.execute.arguments",
        new String[] {
                "env",
                "CLASSPATH=" + getApkName(this),
                "LD_PRELOAD=",
                "LD_LIBRARY_PATH=",
                "/system/bin/app_process",
                "/", "com.termux.plugin.CmdEntryPointStaticClassWithMain",
                "...", "some", "other", "args", "passed", "to", "CmdEntryPointStaticClassWithMain's", "main", "..."
        });
intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", true);
Log.d("Command", "sendRunCommand: " + intent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    startForegroundService(intent);
} else {
    startService(intent);
}

To interact with mother activity com.termux.plugin.CmdEntryPointStaticClassWithMain should obtain Context instance. It can be done by invoking android.app.ActivityThread.systemMain().getSystemContext(). This api is not public, but also it is not restricted so it can be accessed through reflection or using compileOnly dependency describing it like an interface. Example.
After invoking it you will have instance of Context which can send broadcasts with code like that:

Bundle bundle = new Bundle();
bundle.putBinder("", this);

Intent intent = new Intent("some_action");
intent.putExtra("", bundle);
intent.setPackage("com.termux.plugin");
ctx.sendBroadcast(intent);

In my code class com.termux.plugin.CmdEntryPointStaticClassWithMain implemens interface ICmdEntryInterface.Stub so I can share it as a IBinder. No need to worry about mutiple connections.
Also pay attention on the fact I used intent.setPackage and not intent.setClass. I did it to make it work with context-registered BroadcastReceiver.

This way I will be able to work it with the following code.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if ("some_action").equals(intent.getAction())) {
                try {
                    IBinder b = intent.getBundleExtra("").getBinder("");
                    service = ICmdEntryInterface.Stub.asInterface(b);
                    service.asBinder().linkToDeath(() -> {
                        // Code handling death of other side...
                    }, 0);

                    onReceiveConnection();
                } catch (RemoteException e) {
                    // Code handling exceptions
                } catch (NullPointerException e) {
                    // Code handling exceptions
                }
            }
        }
    }, new IntentFilter("some_action"));
}

For some reason sending ParcelFileDescriptor can be done only from service to client so to send it back you will need to pass second IBinder back to implement reverse channel. It does not affect file descriptors sent through Unix sockets. But for some reason Unix server socket opened in com.termux userspace transfered through Binder can come to com.termux.plugin in some broken state. I did not found a way to fix that.

Also both regular and reverse channel IBinders can be used to monitor other side's status. You can bind callback to listen for event of dying of other side's process using linkToDeath function.

But there will be a problem of making it work without allow-external-apps enabled.
I can suggest to make second service like RunCommandService, but not secured with RUN_COMMAND permission. It should get requestor's UID using Binder.getCallingUid(), get name of package using getPackageManager().getNameForUid(uid); and then compare target's and self signature with something like

PackageInfo selfInfo = pm.getPackageInfo("com.termux", PackageManager.GET_SIGNATURES);
PackageInfo targetInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
if (selfInfo.signatures.length != targetInfo.signatures.length
        || selfInfo.signatures[0].hashCode() != targetInfo.signatures[0].hashCode()) {
    Logger.logInfoAndShowToast(this, LOG_TAG, "Signatures of this com.termux and plugin " + targetPackageName + " do not match.");
    Logger.logInfoAndShowToast(this, LOG_TAG, "Please, install plugin from F-Droid/debug builds/etc...");
}

This way you will be able to get rid of sharedUserId and keep compatibility with existing plugins (with some patches). Also it gives much more power to compatible plugins, because they can run in com.termux's userspace almostly unrestricted and the same time it is secure because requestor is checked at start time.

@twaik
Copy link
Member

twaik commented Mar 9, 2023

@agnostic-apollo

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 a pull request may close this issue.

4 participants