Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Unlike target applications which request access, the daemon actively pushes its
### 4. Native Socket IPC
For native components that operate outside the Java Binder context, the daemon provisions two distinct types of UNIX domain sockets.

* Command-Line Interface: The `CliSocketServer` exposes a filesystem-based socket at `/data/adb/lspd/.cli_sock`. The CLI client authenticates using a compiled-in UUID token and communicates using structured JSON. For live log streaming, the daemon attaches the log file's raw `FileDescriptor` to the socket reply payload, allowing the client to read directly from the OS-level stream buffer.
* Command-Line Interface: The `CliSocketServer` binds an abstract UNIX domain socket under a per-boot random name, which the daemon publishes to a root-only file (`/data/adb/lspd/.sock`) for the CLI client to read. This keeps `/proc/net/unix` free of any constant, framework-specific string that an integrity scanner could match. The client authenticates using a compiled-in UUID token and communicates using structured JSON. For live log streaming, the daemon attaches the log file's raw `FileDescriptor` to the socket reply payload, allowing the client to read directly from the OS-level stream buffer.
* Dex2Oat Wrapper: The `Dex2OatServer` listens on an abstract UNIX domain socket. To prevent conflicts and detection, the exact name of this abstract socket is randomized during module installation. The C++ `dex2oat` wrapper connects to this socket to receive necessary file descriptors via `SCM_RIGHTS`.

## Native Environment Subsystems
Expand Down
16 changes: 8 additions & 8 deletions daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ object VectorIPC {
fun transmit(request: CliRequest): CliResponse {
val socket = LocalSocket()
return try {
val cliSocket = FileSystem.socketPath.toString()
val socketFile = java.io.File(cliSocket)

if (!socketFile.exists()) {
System.err.println("Error: Socket file not found at $cliSocket")
System.err.println("Current UID: ${android.os.Process.myUid()}")
}
socket.connect(LocalSocketAddress(cliSocket, LocalSocketAddress.Namespace.FILESYSTEM))
// The daemon binds an abstract socket under a per-boot random name and publishes that name
// through FileSystem; absent it, no daemon is running to talk to.
val socketName =
FileSystem.readSocketName()
?: return CliResponse(
success = false,
error = "Vector daemon is not running (no CLI socket published).")
socket.connect(LocalSocketAddress(socketName, LocalSocketAddress.Namespace.ABSTRACT))

val output = DataOutputStream(socket.outputStream)
val input = DataInputStream(socket.inputStream)
Expand Down
57 changes: 49 additions & 8 deletions daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
import java.nio.file.attribute.PosixFilePermissions
import java.security.SecureRandom
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
Expand Down Expand Up @@ -69,7 +70,6 @@ object FileSystem {
val logDirPath: Path = basePath.resolve("log")
val oldLogDirPath: Path = basePath.resolve("log.old")
val modulePath: Path = basePath.resolve("modules")
val socketPath: Path = basePath.resolve(".cli_sock")
val daemonApkPath: Path = Paths.get(System.getProperty("java.class.path", ""))
val managerApkPath: Path = daemonApkPath.parent.resolve("manager.apk")
val configDirPath: Path = basePath.resolve("config")
Expand All @@ -79,6 +79,26 @@ object FileSystem {

private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault())
private val lockPath: Path = basePath.resolve("lock")

/**
* The root-only file that publishes the name of the abstract socket the CLI reaches the daemon
* on.
*
* The CLI endpoint used to be a fixed filesystem socket at /data/adb/lspd/.cli_sock. The kernel
* copies a socket's bind address verbatim into /proc/net/unix, and that table is readable by any
* process sharing the (on Android, global) network namespace, regardless of the 0700 directory
* guarding the file on disk. So the constant path string was a stable, version-independent
* signature for the framework (#891): a scanner only had to grep /proc/net/unix for it, no
* privilege required, because the leak is the kernel's own bookkeeping rather than any surface
* this daemon controls.
*
* The socket is now bound in the abstract namespace under a per-boot random name, which shows up
* as @<name> with nothing constant to match. The daemon and the CLI are separate processes, so
* the name cannot live only in this object's memory the way the compile-time constant did; the
* daemon writes it to this file — inside the same 0700 root-owned directory, and never itself a
* socket, so it stays out of /proc/net/unix — for the CLI to read back.
*/
private val socketNamePath: Path = basePath.resolve(".sock")
private var fileLock: FileLock? = null
private var lockChannel: FileChannel? = null

Expand All @@ -92,6 +112,11 @@ object FileSystem {
.onFailure { Log.e(TAG, "Failed to initialize directories", it) }
}

/**
* Deploys the CLI helper and mints the name for the abstract socket the daemon is about to bind,
* publishing it to [socketNamePath] so the separate CLI process can find it. Returns the fresh
* name. Called once per daemon start, so each boot advertises a new name.
*/
fun setupCli(): String {
val cliSource = daemonApkPath.parent.resolve("cli").toFile()
val cliDest = basePath.resolve("cli").toFile()
Expand All @@ -103,14 +128,30 @@ object FileSystem {
.onFailure { Log.e(TAG, "Failed to deploy CLI script", it) }
}

val cliSocket: String = socketPath.toString()
val socketFile = File(cliSocket)
if (socketFile.exists()) {
Log.d(TAG, "Existing $cliSocket deleted")
socketFile.delete()
}
val socketName = generateSocketName()
runCatching {
Files.write(socketNamePath, socketName.toByteArray())
Os.chmod(socketNamePath.toString(), "600".toInt(8))
}
.onFailure { Log.e(TAG, "Failed to publish the CLI socket name", it) }
return socketName
}

/**
* The abstract socket name the running daemon published, or null when no daemon has written one
* (so the CLI can say "not running" rather than fail to connect to a stale name). Read fresh at
* connect time so a daemon restart, which rewrites the file, is picked up without coordination.
*/
fun readSocketName(): String? =
runCatching { Files.readAllBytes(socketNamePath).toString(Charsets.UTF_8).trim() }
.getOrNull()
?.takeIf { it.isNotEmpty() }

return cliSocket
/** 128 random bits as hex: unguessable, and no constant a scanner can key on. */
private fun generateSocketName(): String {
val bytes = ByteArray(16)
SecureRandom().nextBytes(bytes)
return bytes.joinToString("") { "%02x".format(it.toInt() and 0xff) }
}

/** Tries to lock the daemon lockfile. Returns false if another daemon is running. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.system.Os
import android.util.Log
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import kotlinx.coroutines.launch
Expand All @@ -29,24 +28,25 @@ object CliSocketServer {
// Keep these references outside the loop to prevent GC from closing them
var rootSocket: LocalSocket? = null
var server: LocalServerSocket? = null
var socketFile: File? = null

try {
val cliSocketPath: String = FileSystem.setupCli()
socketFile = File(cliSocketPath)
val socketName: String = FileSystem.setupCli()

// Create a standard LocalSocket
rootSocket = LocalSocket()
// Bind it to the filesystem path
val address = LocalSocketAddress(cliSocketPath, LocalSocketAddress.Namespace.FILESYSTEM)
// Bind it in the abstract namespace: the name shows up in /proc/net/unix as @<name> with
// no filesystem path, and the daemon mints a fresh random name every boot, so there is no
// constant string left for an integrity scanner to fingerprint (#891). The bind carries no
// file, so nothing has to be unlinked when the socket closes either.
val address = LocalSocketAddress(socketName, LocalSocketAddress.Namespace.ABSTRACT)
rootSocket.bind(address)

// LocalServerSocket(FileDescriptor) requires the FD to already be listening.
Os.listen(rootSocket.fileDescriptor, 50)
// Wrap the underlying FileDescriptor into a ServerSocket
server = LocalServerSocket(rootSocket.fileDescriptor)

Log.d(TAG, "CLI server started at $cliSocketPath")
Log.d(TAG, "CLI server started")

while (!Thread.currentThread().isInterrupted) {
try {
Expand All @@ -61,13 +61,11 @@ object CliSocketServer {
Log.e(TAG, "Fatal CLI Server error", e)
} finally {
try {
// Closing the bound fd drops the abstract socket; there is no file to unlink.
server?.close()
rootSocket?.close()
} catch (ignored: Exception) {}

if (socketFile?.exists() == true) {
socketFile.delete()
}
isRunning = false
Log.d(TAG, "CLI server stopped")
}
Expand Down
Loading