Added: LocalFilesystemSocket as an Interface to UNIX sockets in the filesystem. The UID of connecting programs is automatically checked against the processes UID and connections where the UID doesn't match are automatically rejected and logged.

Changed: LocalSocketListener now uses sockets in the filesystem.
This commit is contained in:
tareksander
2021-12-06 17:51:14 +01:00
committed by agnostic-apollo
parent 4aca16326c
commit f366db0cb3
6 changed files with 517 additions and 82 deletions

View File

@@ -4,15 +4,15 @@ import android.app.Application;
import android.content.Context;
import com.termux.am.Am;
import com.termux.shared.logger.Logger;
import com.termux.shared.shell.LocalSocketListener;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.crash.TermuxCrashUtils;
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.termux.theme.TermuxThemeUtils;
import com.termux.shared.shell.LocalSocketListener;
import java.io.IOException;
import java.io.File;
public class TermuxApplication extends Application {
@@ -35,18 +35,15 @@ public class TermuxApplication extends Application {
// Set NightMode.APP_NIGHT_MODE
TermuxThemeUtils.setAppNightMode(properties.getNightMode());
try {
new LocalSocketListener(this, (args, out, err) -> {
try {
new Am(out, err, this).run(args);
return 0;
} catch (Exception e) {
return 1;
}
}, TermuxConstants.TERMUX_PACKAGE_NAME+"://call-am", 1000);
}
catch (IOException e) {
Logger.logDebug("TermuxApplication", "am socket already in use");
if (LocalSocketListener.tryEstablishLocalSocketListener(this, (args, out, err) -> {
try {
new Am(out, err, this).run(args);
return 0;
} catch (Exception e) {
return 1;
}
}, new File(getFilesDir(), "am-socket").getAbsolutePath(), 100, 1000) == null) {
Logger.logWarn("TermuxApplication", "am socket cannot be created");
}
}

View File

@@ -32,6 +32,11 @@ android {
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
ndkBuild {
cppFlags ''
}
}
}
buildTypes {
@@ -45,6 +50,11 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
ndkBuild {
path file('src/main/cpp/Android.mk')
}
}
}
dependencies {

View File

@@ -0,0 +1,6 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS := -llog
LOCAL_MODULE := local-filesystem-socket
LOCAL_SRC_FILES := local-filesystem-socket.cpp
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1,196 @@
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/ioctl.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <cerrno>
#include <jni.h>
#include <android/log.h>
int64_t millisecondtime(const struct timespec* const time) {
return (((int64_t)time->tv_sec)*1000)+(((int64_t)time->tv_nsec)/1000000);
}
/// Sets the timeout in microseconds
void settimeout_micro(int fd, int timeout) {
struct timeval t = {};
t.tv_sec = 0;
t.tv_usec = timeout;
socklen_t len = sizeof(t);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &t, len);
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &t, len);
}
/// Sets the timeout in milliseconds
void settimeout(int fd, int timeout) {
settimeout_micro(fd, timeout*1000);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_createserversocket(JNIEnv *env, jclass clazz, jbyteArray path, jint backlog) {
if (backlog < 1) {
return -1;
}
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1) {
return -1;
}
jbyte* p = env->GetByteArrayElements(path, nullptr);
if (p == nullptr) {
close(fd);
return -1;
}
int chars = env->GetArrayLength(path);
if (chars >= sizeof(struct sockaddr_un)-sizeof(sa_family_t)) {
env->ReleaseByteArrayElements(path, p, JNI_ABORT);
close(fd);
return -1;
}
struct sockaddr_un adr = {.sun_family = AF_UNIX};
memcpy(&adr.sun_path, p, chars);
if (bind(fd, reinterpret_cast<struct sockaddr*>(&adr), sizeof(adr)) == -1) {
env->ReleaseByteArrayElements(path, p, JNI_ABORT);
close(fd);
return -1;
}
if (listen(fd, backlog) == -1) {
env->ReleaseByteArrayElements(path, p, JNI_ABORT);
close(fd);
return -1;
}
return fd;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_closefd(JNIEnv *env, jclass clazz, jint fd) {
close(fd);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_accept(JNIEnv *env, jclass clazz, jint fd) {
int c = accept(fd, nullptr, nullptr);
if (c == -1) {
return -1;
}
return c;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_send(JNIEnv *env, jclass clazz, jbyteArray data, jint fd, jlong deadline) {
if (fd == -1) {
return false;
}
jbyte* d = env->GetByteArrayElements(data, nullptr);
if (d == nullptr) {
return false;
}
struct timespec time = {};
jbyte* current = d;
int bytes = env->GetArrayLength(data);
while (bytes > 0) {
if (clock_gettime(CLOCK_REALTIME, &time) != -1) {
if (millisecondtime(&time) > deadline) {
env->ReleaseByteArrayElements(data, d, JNI_ABORT);
return false;
}
} else {
__android_log_write(ANDROID_LOG_WARN, "LocalFilesystemSocket.send", "Could not get the current time, deadline will not work");
}
int ret = send(fd, current, bytes, MSG_NOSIGNAL);
if (ret == -1) {
__android_log_print(ANDROID_LOG_DEBUG, "LocalFilesystemSocket.send", "%s", strerror(errno));
env->ReleaseByteArrayElements(data, d, JNI_ABORT);
return false;
}
bytes -= ret;
current += ret;
}
env->ReleaseByteArrayElements(data, d, JNI_ABORT);
return true;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_recv(JNIEnv *env, jclass clazz, jbyteArray data, jint fd, jlong deadline) {
if (fd == -1) {
return -1;
}
jbyte* d = env->GetByteArrayElements(data, nullptr);
if (d == nullptr) {
return -1;
}
struct timespec time = {};
jbyte* current = d;
int bytes = env->GetArrayLength(data);
int transferred = 0;
while (transferred < bytes) {
if (clock_gettime(CLOCK_REALTIME, &time) != -1) {
if (millisecondtime(&time) > deadline) {
env->ReleaseByteArrayElements(data, d, 0);
return -1;
}
} else {
__android_log_write(ANDROID_LOG_WARN, "LocalFilesystemSocket.recv", "Could not get the current time, deadline will not work");
}
int ret = read(fd, current, bytes);
if (ret == -1) {
env->ReleaseByteArrayElements(data, d, 0);
return -1;
}
// EOF, peer closed writing end
if (ret == 0) {
break;
}
transferred += ret;
current += ret;
}
env->ReleaseByteArrayElements(data, d, 0);
return transferred;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_settimeout(JNIEnv *env, jclass clazz, jint fd, jint timeout) {
settimeout(fd, timeout);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_available(JNIEnv *env, jclass clazz, jint fd) {
int size = 0;
if (ioctl(fd, SIOCINQ, &size) == -1) {
return 0;
}
return size;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_termux_shared_shell_LocalFilesystemSocket_getpeeruid(JNIEnv *env, jclass clazz, jint fd) {
struct ucred cred = {};
socklen_t len = sizeof(cred);
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) {
return -1;
}
return cred.uid;
}

View File

@@ -0,0 +1,269 @@
package com.termux.shared.shell;
import android.content.Context;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class LocalFilesystemSocket
{
static {
System.loadLibrary("local-filesystem-socket");
}
/**
* Creates as UNIX server socket at {@code path}, with a backlog of {@code backlog}.
* @return The file descriptor of the server socket or -1 if an error occurred.
*/
private static native int createserversocket(@NonNull byte[] path, int backlog);
/**
* Accepts a connection on the supplied server socket.
* @return The file descriptor of the connection socket or -1 in case of an error.
*/
private static native int accept(int fd);
private static native void closefd(int fd);
/**
* Returns the UID of the socket peer, or -1 in case of an error.
*/
private static native int getpeeruid(int fd);
/**
* Sends {@code data} over the socket and decrements the timeout by the elapsed time. If the timeout hits or an error occurs, false is returned, true otherwise.
*/
private static native boolean send(@NonNull byte[] data, int fd, long deadline);
/**
* Receives data from the socket and decrements the timeout by the elapsed time. If the timeout hits or an error occurs, -1 is returned, otherwise the number of received bytes.
*/
private static native int recv(@NonNull byte[] data, int fd, long deadline);
/**
* Sets the send and receive timeout for the socket in milliseconds.
*/
private static native void settimeout(int fd, int timeout);
/**
* Gets the number of bytes available to read on the socket.
*/
private static native int available(int fd);
static class Socket implements Closeable {
private int fd;
private long deadline = 0;
private final SocketOutputStream out = new SocketOutputStream();
private final SocketInputStream in = new SocketInputStream();
class SocketInputStream extends InputStream {
private final byte[] readb = new byte[1];
@Override
public int read() throws IOException {
int ret = recv(readb);
if (ret == -1) {
throw new IOException("Could not read from socket");
}
if (ret == 0) {
return -1;
}
return readb[0];
}
@Override
public int read(byte[] b) throws IOException {
if (b == null) {
throw new NullPointerException("Read buffer can't be null");
}
int ret = recv(b);
if (ret == -1) {
throw new IOException("Could not read from socket");
}
if (ret == 0) {
return -1;
}
return ret;
}
@Override
public int available() {
return Socket.this.available();
}
}
class SocketOutputStream extends OutputStream {
private final byte[] writeb = new byte[1];
@Override
public void write(int b) throws IOException {
writeb[0] = (byte) b;
if (! send(writeb)) {
throw new IOException("Could not write to socket");
}
}
@Override
public void write(byte[] b) throws IOException {
if (! send(b)) {
throw new IOException("Could not write to socket");
}
}
}
private Socket(int fd) {
this.fd = fd;
}
/**
* Sets the socket timeout, that makes a single send/recv error out if it triggers. For a deadline after which the socket should be finished see setDeadline()
*/
public void setTimeout(int timeout) {
if (fd != -1) {
settimeout(fd, timeout);
}
}
/**
* Sets the deadline in unix milliseconds. When the deadline has elapsed and the socket timeout triggers, all reads and writes will error.
*/
public void setDeadline(long deadline) {
this.deadline = deadline;
}
public boolean send(@NonNull byte[] data) {
if (fd == -1) {
return false;
}
return LocalFilesystemSocket.send(data, fd, deadline);
}
public int recv(@NonNull byte[] data) {
if (fd == -1) {
return -1;
}
return LocalFilesystemSocket.recv(data, fd, deadline);
}
public int available() {
if (fd == -1 || System.currentTimeMillis() > deadline) {
return 0;
}
return LocalFilesystemSocket.available(fd);
}
@Override
public void close() throws IOException {
if (fd != -1) {
closefd(fd);
fd = -1;
}
}
/**
* Returns the UID of the socket peer, or -1 in case of an error.
*/
public int getPeerUID() {
return getpeeruid(fd);
}
/**
* Returns an {@link OutputStream} for the socket. You don't need to close the stream, it's automatically closed when closing the socket.
*/
public OutputStream getOutputStream() {
return out;
}
/**
* Returns an {@link InputStream} for the socket. You don't need to close the stream, it's automatically closed when closing the socket.
*/
public InputStream getInputStream() {
return in;
}
}
static class ServerSocket implements Closeable
{
private final String path;
private final Context app;
private int fd;
public ServerSocket(Context c, String path, int backlog) throws IOException {
app = c.getApplicationContext();
if (backlog <= 0) {
throw new IllegalArgumentException("Backlog has to be at least 1");
}
if (path == null) {
throw new IllegalArgumentException("path cannot be null");
}
this.path = path;
File f = new File(path);
File parent = f.getParentFile();
if (parent != null) {
parent.mkdirs();
}
f.delete();
fd = createserversocket(path.getBytes(StandardCharsets.UTF_8), backlog);
if (fd == -1) {
throw new IOException("Could not create UNIX server socket at \""+path+"\"");
}
}
public ServerSocket(Context c, String path) throws IOException {
this(c, path, 50); // 50 is the default value for the Android LocalSocket implementation, so this should be good here, too
}
public Socket accept() {
if (fd == -1) {
return null;
}
int c = -1;
while (true) {
while (c == -1) {
c = LocalFilesystemSocket.accept(fd);
}
int peeruid = getpeeruid(c);
if (peeruid == -1) {
Logger.logWarn("LocalFilesystemSocket.ServerSocket", "Could not verify peer uid, closing socket");
closefd(c);
c = -1;
continue;
}
if (peeruid == app.getApplicationInfo().uid) {
// if the peer has the same uid, allow the connection
break;
} else {
Logger.logWarn("LocalFilesystemSocket.ServerSocket", "WARNING: An app with the uid of "+peeruid+" tried to connect to the socket at \""+path+"\", closing connection.");
closefd(c);
c = -1;
}
}
return new Socket(c);
}
@Override
public void close() throws IOException {
if (fd != -1) {
closefd(fd);
fd = -1;
}
}
}
}

View File

@@ -1,8 +1,6 @@
package com.termux.shared.shell;
import android.app.Application;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import androidx.annotation.NonNull;
@@ -22,21 +20,31 @@ public class LocalSocketListener
int handle(String[] args, PrintStream out, PrintStream err);
}
private static final String LOG_TAG = "LocalSocketListener";
private final Thread thread;
private final LocalServerSocket server;
private final LocalFilesystemSocket.ServerSocket server;
private final int timeoutMillis;
private final int deadlineMillis;
public LocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, String address, int timeoutMillis) throws IOException {
private LocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, @NonNull String path, int timeoutMillis, int deadlineMillis) throws IOException {
this.timeoutMillis = timeoutMillis;
server = new LocalServerSocket(address);
thread = new Thread(new LocalSocketListenerRunnable(a, h));
this.deadlineMillis = deadlineMillis;
server = new LocalFilesystemSocket.ServerSocket(a, path);
thread = new Thread(new LocalSocketListenerRunnable(h));
thread.setUncaughtExceptionHandler((t, e) -> Logger.logStackTraceWithMessage(LOG_TAG, "Uncaught exception in LocalSocketListenerRunnable", e));
thread.start();
}
public static LocalSocketListener tryEstablishLocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, @NonNull String address, int timeoutMillis, int deadlineMillis) {
try {
return new LocalSocketListener(a, h, address, timeoutMillis, deadlineMillis);
} catch (IOException e) {
return null;
}
}
@SuppressWarnings("unused")
public void stop() {
try {
@@ -46,78 +54,30 @@ public class LocalSocketListener
}
private class LocalSocketListenerRunnable implements Runnable {
private final Application a;
private final TimeoutWatcher timeoutWatcher;
private final Thread timeoutWatcherThread;
private final LocalSocketHandler h;
public LocalSocketListenerRunnable(@NonNull Application a, @NonNull LocalSocketHandler h) {
this.a = a;
public LocalSocketListenerRunnable(@NonNull LocalSocketHandler h) {
this.h = h;
timeoutWatcher = new TimeoutWatcher();
timeoutWatcherThread = new Thread(timeoutWatcher);
timeoutWatcherThread.start();
}
// the socket timeout for LocalSocket doesn't seem to work, so close the socket if the timeout is over, so the processing Thread doesn't get blocked.
private class TimeoutWatcher implements Runnable {
private final Object lock = new Object();
private LocalSocket current = null;
@Override
public void run() {
while (! Thread.currentThread().isInterrupted()) {
LocalSocket watch = current;
synchronized (lock) {
while (watch == null) {
try {
lock.wait();
}
catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
watch = current;
}
}
try {
//noinspection BusyWait
Thread.sleep(timeoutMillis);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
try {
watch.shutdownInput();
} catch (Exception ignored) {}
try {
watch.shutdownOutput();
} catch (Exception ignored) {}
try {
watch.close();
} catch (Exception ignored) {}
}
}
}
@Override
public void run() {
try {
while (! Thread.currentThread().isInterrupted()) {
try (LocalSocket s = server.accept();
try (LocalFilesystemSocket.Socket s = server.accept();
OutputStream sockout = s.getOutputStream();
InputStreamReader r = new InputStreamReader(s.getInputStream())) {
timeoutWatcher.current = s;
synchronized (timeoutWatcher.lock) {
timeoutWatcher.lock.notifyAll();
}
// ensure only Termux programs can connect
if (s.getPeerCredentials().getUid() != a.getApplicationInfo().uid) {
Logger.logDebug(LOG_TAG, "A program with another UID tried to connect");
continue;
}
s.setTimeout(timeoutMillis);
s.setDeadline(System.currentTimeMillis()+deadlineMillis);
StringBuilder b = new StringBuilder();
int c;
while ((c = r.read()) > 0) {
b.append((char) c);
}
Logger.logDebug(LOG_TAG, b.toString());
String outString;
String errString;
int ret;
@@ -151,9 +111,6 @@ public class LocalSocketListener
try {
server.close();
} catch (Exception ignored) {}
if (timeoutWatcherThread.isAlive()) {
timeoutWatcherThread.interrupt();
}
}
Logger.logDebug(LOG_TAG, "LocalSocketListenerRunnable returned");
}