Skip to content

[Feature request] Getting rid of sharedUserId #668

Open
@twaik

Description

@twaik

Feature description

Lot of users have problems with sharedUserId.
Termux:X11 a lot of time does not have this problem.
Probably it will be good for Termux:API to implement something similar.

Connection establishing problem

Currently the most complicated problem is establishing connection.
termux-api command creates two abstract sockets and executes am broadcast with passing names of these abstract sockets as an arguments (extras).
It is problematic for two reasons.

  1. We can not pass abstract sockets bypassing android application sandbox because of selinux restrictions.
  2. We can not pin abstract or any other type of socket to intent because of am restrictions.

I see 2 ways to handle this.

First way

You can use some Java code (yeah, invoking app_process for this) to send broadcast with pinned binder, which will return file descriptors of sockets (probably created by socketpair, abstract sockets can not be shared this way) created by termux-api command when Termux:API's BroadcastReceiver invokes this Binder. Something like this:

some code
    /** @noinspection DataFlowIssue*/
    @SuppressLint("DiscouragedPrivateApi")
    public static Context createContext() {
        Context context = null;
        PrintStream err = System.err;
        try {
            // `android.app.ActivityThread.systemMain().getSystemContext()` is not used to avoid `java.lang.RuntimeException: Unable to instantiate Application():android.content.res.Resources$NotFoundException: Resource ID #0x600a6`
            java.lang.reflect.Field f = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Object unsafe = f.get(null);
            // Hiding harmless framework errors, like this:
            // java.io.FileNotFoundException: /data/system/theme_config/theme_compatibility.xml: open failed: ENOENT (No such file or directory)
            System.setErr(new PrintStream(new OutputStream() { public void write(int arg0) {} }));
            context = ((android.app.ActivityThread) Class.
                    forName("sun.misc.Unsafe").
                    getMethod("allocateInstance", Class.class).
                    invoke(unsafe, android.app.ActivityThread.class))
                    .getSystemContext();
        } catch (Exception e) {
            Log.e("Context", "Failed to instantiate context:", e);
            context = null;
        } finally {
            System.setErr(err);
        }
        return context;
    }

    @SuppressLint({"WrongConstant", "PrivateApi"})
    void sendBroadcast() {
        Bundle bundle = new Bundle();
        bundle.putBinder("", new Binder() {
            @Override
            protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
                if (code == Binder.FIRST_CALL_TRANSACTION) {
                    /// ... Sending ParcelFileDescriptors obtained with `adoptFd` calls
                }
                return true;
            }
        });

        Intent intent = new Intent(ACTION_START);
        intent.putExtra("", bundle);
        intent.setPackage("com.termux.api");

        if (getuid() == 0 || getuid() == 2000)
            intent.setFlags(0x00400000 /* FLAG_RECEIVER_FROM_SHELL */);

        try {
            ctx.sendBroadcast(intent);
        } catch (Exception e) {
            if (e instanceof NullPointerException && ctx == null)
                Log.i("Broadcast", "Context is null, falling back to manual broadcasting");
            else
                Log.e("Broadcast", "Falling back to manual broadcasting, failed to broadcast intent through Context:", e);

            String packageName;
            try {
                packageName = android.app.ActivityThread.getPackageManager().getPackagesForUid(getuid())[0];
            } catch (RemoteException ex) {
                throw new RuntimeException(ex);
            }
            IActivityManager am;
            try {
                //noinspection JavaReflectionMemberAccess
                am = (IActivityManager) android.app.ActivityManager.class
                        .getMethod("getService")
                        .invoke(null);
            } catch (Exception e2) {
                try {
                    am = (IActivityManager) Class.forName("android.app.ActivityManagerNative")
                            .getMethod("getDefault")
                            .invoke(null);
                } catch (Exception e3) {
                    throw new RuntimeException(e3);
                }
            }

            assert am != null;
            IIntentSender sender = am.getIntentSender(1, packageName, null, null, 0, new Intent[] { intent },
                    null, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT, null, 0);
            try {
                //noinspection JavaReflectionMemberAccess
                IIntentSender.class
                        .getMethod("send", int.class, Intent.class, String.class, IBinder.class, IIntentReceiver.class, String.class, Bundle.class)
                        .invoke(sender, 0, intent, null, null, new IIntentReceiver.Stub() {
                            @Override public void performReceive(Intent i, int r, String d, Bundle e, boolean o, boolean s, int a) {}
                        }, null, null);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
    }

This code is got from Termux:X11, but maybe most of it may be replaced with similar code from TermuxAm.

This code must be called on every termux-api invocation only in the case if socketpair created sockets are used. In the case if Java code will create server socket (i.e. in $TMPDIR/termux-api-socket) and stay in background (and will send sockets of newly-created connections to Termux:API through Broadcasts or already-established socketpair connection) you will need to launch this code only once. termux-api can check if Unix socket in $TMPDIR/termux-api-socket is connectable and if there is a lock file that should be created by Java code. OF COURSE this java code can be used to verify if we use Termux:API apk with right signature (hardcoded on compilation stage).

// Java
android.content.pm.PackageInfo targetInfo = (android.os.Build.VERSION.SDK_INT <= 32) ?
    android.app.ActivityThread.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, android.content.pm.PackageManager.GET_SIGNATURES, 0) :
    android.app.ActivityThread.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, (long) android.content.pm.PackageManager.GET_SIGNATURES, 0);
assert targetInfo.signatures.length == 1 && BuildConfig.SIGNATURE == targetInfo.signatures[0].hashCode() : packageSignatureMismatchErrorText;
// build.gradle
android.defaultConfig.buildConfigField "int", "SIGNATURE", String.valueOf(Arrays.hashCode(keyStore.getCertificate(signingConfig.keyAlias).getEncoded()))

Second way

You can add an ability to pass file descriptors through Binder to regular TermuxAm and termux-am-socket in similar way. In this case we will be able to go on using am broadcast command.
But in this case you can not verify if right Termux:API apk is installed. Or probably you may integrate signature verification to both TermuxAm and termux-am-socket too :).

Restrictions

That will be problematic for root users. For some reason SELinux blocks Unix sockets from being passed through Binder. Probably you will need to create two pipes (one for reading and one for writing) and pass one side of both pipes through Binder.

File access problem

In the case of disabling sharedUserId Termux:API will lose access to files in $PREFIX and $HOME.
I am proposing to not send stdin and stdout contents in both ways directly. Instead of that you can implement some simple command system to send buffers got from stdin/stdout, create/modify/delete files, pass file descriptors (i.e. for termux-usb) and do other stuff.
To avoid creating complicated IPC system you can go libxcb way and pass smth like union Event which will contain all possible events. I did this in Termux:X11:

some code
typedef enum {
    EVENT_SCREEN_SIZE,
    EVENT_TOUCH,
    EVENT_MOUSE,
    EVENT_KEY,
    EVENT_UNICODE,
    EVENT_CLIPBOARD_ENABLE,
    EVENT_CLIPBOARD_ANNOUNCE,
    EVENT_CLIPBOARD_REQUEST,
    EVENT_CLIPBOARD_SEND,
} eventType;
typedef union {
    uint8_t type;
    struct {
        uint8_t t;
        uint16_t width, height, framerate;
    } screenSize;
    struct {
        uint8_t t;
        uint16_t type, id, x, y;
    } touch;
    struct {
        uint8_t t;
        float x, y;
        uint8_t detail, down, relative;
    } mouse;
    struct {
        uint8_t t;
        uint16_t key;
        uint8_t state;
    } key;
    struct {
        uint8_t t;
        uint32_t code;
    } unicode;
    struct {
        uint8_t t;
        uint8_t enable;
    } clipboardEnable;
    struct {
        uint8_t t;
        uint32_t count;
    } clipboardSend;
} lorieEvent;

// and some code to handle that 
JNIEXPORT void JNICALL
Java_com_termux_x11_LorieView_handleXEvents(JNIEnv *env, jobject thiz) {
    checkConnection(env);
    if (conn_fd != -1) {
        lorieEvent e = {0};

        again:
        if (read(conn_fd, &e, sizeof(e)) == sizeof(e)) {
            switch(e.type) {
                case EVENT_CLIPBOARD_SEND: {
                    char clipboard[e.clipboardSend.count + 1];
                    read(conn_fd, clipboard, sizeof(clipboard));
                    clipboard[e.clipboardSend.count] = 0;
                    log(DEBUG, "Clipboard content (%zu symbols) is %s", strlen(clipboard), clipboard);
                    jmethodID id = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, thiz), "setClipboardText","(Ljava/lang/String;)V");
                    jobject bb = (*env)->NewDirectByteBuffer(env, clipboard, strlen(clipboard));
                    jobject charset = (*env)->CallStaticObjectMethod(env, Charset.self, Charset.forName, (*env)->NewStringUTF(env, "UTF-8"));
                    jobject cb = (*env)->CallObjectMethod(env, charset, Charset.decode, bb);
                    (*env)->DeleteLocalRef(env, bb);

                    jstring str = (*env)->CallObjectMethod(env, cb, CharBuffer.toString);
                    (*env)->CallVoidMethod(env, thiz, id, str);
                    break;
                }
                case EVENT_CLIPBOARD_REQUEST: {
                    (*env)->CallVoidMethod(env, thiz, (*env)->GetMethodID(env, (*env)->GetObjectClass(env, thiz), "requestClipboard", "()V"));
                    break;
                }
            }
        }

        int n;
        if (ioctl(conn_fd, FIONREAD, &n) >= 0 && n > sizeof(e))
            goto again;
    }
}

This code can be combined with poll or select to not create threads for handling stdin/stdout/socket events separately.

In the case if you need to send/receive some additional data you can simply use `write`/`read` or `sendv`/`recv` (for the case when you pass file descriptors) after reading the `Event` struct/union.

@tareksander probably all of that is applicable to termux-gui too since it is based on termux-api and it is incompatible with F-Droid builds of Termux and its plugins.
Ping @agnostic-apollo @Grimler91 @tareksander.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions