diff --git a/.github/workflows/swift-arm.yml b/.github/workflows/swift-arm.yml index 5fc04f3..ed70cc3 100644 --- a/.github/workflows/swift-arm.yml +++ b/.github/workflows/swift-arm.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: config: ["debug" , "release"] - swift: ["6.1.2"] + swift: ["6.3.3"] linux: ["raspios"] release: ["bookworm"] arch: ["armv6", "armv7"] @@ -20,7 +20,7 @@ jobs: run: apt update -y; apt install wget -y - name: Install SDK run: | - wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz + wget https://github.com/swift-embedded-linux/armhf-debian/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz tar -xvf swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz mv swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} - name: Swift Version @@ -34,7 +34,7 @@ jobs: strategy: matrix: config: ["debug" , "release"] - swift: ["6.1.2"] + swift: ["6.3.3"] linux: ["debian"] release: ["bookworm", "bullseye"] arch: ["armv7"] @@ -46,7 +46,7 @@ jobs: run: apt update -y; apt install wget -y - name: Install SDK run: | - wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz + wget https://github.com/swift-embedded-linux/armhf-debian/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz tar -xvf swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz mv swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} - name: Swift Version @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - swift: ["6.0.3"] + swift: ["6.3.3"] container: swift:${{ matrix.swift }} steps: - name: Checkout @@ -73,7 +73,7 @@ jobs: run: apt update -y; apt install wget -y - name: Install SDK run: | - wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7-sdk.tar.gz + wget https://github.com/swift-embedded-linux/armhf-debian/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7-sdk.tar.gz tar -xvf swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7-sdk.tar.gz mv swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7 /opt/swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7 - name: Swift Version diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index d658cf2..e479e89 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -1,5 +1,10 @@ name: Swift on: [push] + +env: + # The single toolchain the whole matrix builds against. Bump here. + SWIFT_VERSION: "6.3.3" + jobs: macos: @@ -12,22 +17,34 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + # The macos-15 runner's bundled Swift trails the pinned version, so the + # toolchain is installed rather than taken from Xcode. SwiftyLab/setup-swift + # resolves versions from swift.org dynamically via swiftly, so it can + # install a release the older swift-actions/setup-swift does not yet list. + - name: Install Swift ${{ env.SWIFT_VERSION }} + uses: SwiftyLab/setup-swift@latest + with: + swift-version: ${{ env.SWIFT_VERSION }} - name: Swift Version run: swift --version - name: Build run: ${{ matrix.options }} swift build -c ${{ matrix.config }} - name: Test run: ${{ matrix.options }} swift test -c ${{ matrix.config }} - + linux: name: Linux strategy: + fail-fast: false matrix: - container: ["swift:6.0.3", "swift:6.1.2"] + # x86_64 and native arm64. arm64 is the architecture Android and most + # binder targets actually run on, so testing there rather than only + # cross-compiling to it is worth a runner. + os: ["ubuntu-latest", "ubuntu-24.04-arm"] config: ["debug", "release"] options: ["", "SWIFT_BUILD_DYNAMIC_LIBRARY=1"] - runs-on: ubuntu-latest - container: ${{ matrix.container }}-jammy + runs-on: ${{ matrix.os }} + container: swift:6.3.3 steps: - name: Checkout uses: actions/checkout@v4 @@ -35,20 +52,56 @@ jobs: run: swift --version - name: Build run: ${{ matrix.options }} swift build -c ${{ matrix.config }} + # The suite runs the whole protocol layer without a binder device: the + # two tests that need /dev/binder skip when it is absent, which it is on + # a CI runner. So Linux, the platform this library actually targets, now + # tests rather than only building. + - name: Test + run: ${{ matrix.options }} swift test -c ${{ matrix.config }} - android-arm: - name: Android - strategy: - fail-fast: false - matrix: - swift: ['6.1', 'nightly-6.2'] - arch: ["aarch64", "x86_64"] - runs-on: macos-15 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - name: "Build Swift Package for Android" - run: | - brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip) - skip android sdk install --version ${{ matrix.swift }} - ANDROID_NDK_ROOT="" skip android build --arch ${{ matrix.arch }} \ No newline at end of file + android: + name: Android + strategy: + fail-fast: false + matrix: + arch: ["aarch64", "x86_64"] + runs-on: macos-15 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: "Build Swift Package for Android" + run: | + brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip) + skip android sdk install --version ${{ env.SWIFT_VERSION }} + ANDROID_NDK_ROOT="" skip android build --arch ${{ matrix.arch }} + + # Cross-compile against Apple's Static Linux SDK, which is musl-based. This + # is what exercises the musl path in CBinder — the kernel uapi headers are + # absent from the musl sysroot, so the C shim falls back to defining the + # __uN types itself and taking the ioctl macros from . + musl: + name: Linux (musl) + strategy: + fail-fast: false + matrix: + arch: ["aarch64", "x86_64"] + runs-on: ubuntu-latest + container: swift:6.3.3 + env: + # The Static Linux SDK bundle for SWIFT_VERSION. The checksum is + # published in swift.org's releases.json and must be updated alongside + # SWIFT_VERSION — it is specific to the exact bundle. + STATIC_SDK_URL: "https://download.swift.org/swift-6.3.3-release/static-sdk/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE_static-linux-0.1.0.artifactbundle.tar.gz" + STATIC_SDK_CHECKSUM: "87c3eaf908e67c0e13a84367119e12273cec1d2cd3d81f7d74bb36722d6b607b" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Swift Version + run: swift --version + - name: Install the Static Linux SDK + run: swift sdk install "$STATIC_SDK_URL" --checksum "$STATIC_SDK_CHECKSUM" + - name: Build + # A binder client is not statically linkable in practice, but the point + # here is to compile the source against musl, so the ordinary product + # build is what matters. + run: swift build --swift-sdk ${{ matrix.arch }}-swift-linux-musl diff --git a/Package.swift b/Package.swift index fe8eae8..c586d3a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.3 import PackageDescription import class Foundation.ProcessInfo @@ -9,10 +9,15 @@ let libraryType: PackageDescription.Product.Library.LibraryType? = dynamicLibrar let package = Package( name: "Binder", platforms: [ - .macOS(.v10_15), - .iOS(.v13), - .watchOS(.v6), - .tvOS(.v13), + // Raised for Synchronization.Mutex, which the serving loop and the + // object registries use and which is macOS 15 / iOS 18 / watchOS 11 / + // tvOS 18 or newer. These are the Darwin mocking-build minimums; the + // real target is Linux and Android, where the version gate does not + // apply. + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), ], products: [ .library( diff --git a/README.md b/README.md index 0e2d9dc..caf079e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,146 @@ # Binder -Swift Android Binder library + +A Swift client for the Android Binder IPC driver (`/dev/binder`), talking the +same protocol as `libbinder` so it can call, and be called by, Android services +— on Android itself or on desktop Linux driving a container such as Waydroid. + +Pure Swift, on `apple/swift-system` and `PureSwift/Socket`. No C++, no +`libbinder`. + +## Status + +The full protocol layer is implemented and unit-tested: the parcel wire format, +the `BINDER_WRITE_READ` command codec, transactions in both directions, the +service manager, reference counting, hosting local objects, a serving loop, and +death notifications. + +**None of it has run against a real driver.** The test host has no binder +kernel module, so every ioctl path is reasoned from the kernel source and +`libbinder` rather than observed. The tests cover encoding, decoding, the object +registries and the loop's dispatch decisions — not the kernel conversation. Treat +this as protocol-complete and field-unproven. + +## Calling a service + +Every AIDL call has the same envelope around its arguments — interface token, +transact, exception header, then the results. The `call` helper writes that +envelope so a method supplies only its code and arguments: + +```swift +let binder = try BinderConnection() + +guard let activity = try binder.service(named: "activity") else { return } + +var reader = try activity.call(1, interface: "android.app.IActivityManager") { + $0.append("com.example") +} +let result = try reader.readString() // reader starts at the results +``` + +The envelope is still there if you want it. The above is exactly: + +```swift +var request = Parcel() +request.appendInterfaceToken("android.app.IActivityManager") +request.append("com.example") + +let reply = try activity.transact(code: 1, request: request) +var reader = reply.reader() +let exception = try reader.readInt32() // non-zero throws remoteException in call() +let result = try reader.readString() +``` + +`post` is the one-way form (no reply); `call` also has an `async` overload. + +`transact`, `service`, `checkService` and `services` each have an `async` +overload. They are not natively asynchronous — a binder transaction blocks in the +driver until the peer replies — so the async forms move that blocking call off +the cooperative pool onto the connection's own queue. See +`Transaction+Async.swift` for what that costs. + +A looked-up service is a `RemoteProxy`: it owns one strong reference and releases +it on `deinit`, so the driver's reference count and Swift's object graph stay in +step without manual `acquire`/`release`. + +## Hosting a service + +```swift +let thing = binder.makeLocalObject(interface: "com.example.IThing") { transaction in + guard transaction.senderUserID == 1000 else { // driver-supplied, unforgeable + return Parcel.exception(-1) // EX_SECURITY + } + do { + var args = try transaction.arguments(interface: "com.example.IThing") + let name = try args.readString() ?? "" + return Parcel.reply { $0.append("hello:\(name)") } + } catch { + return Parcel.exception(-1) + } +} + +try binder.addService(named: "com.example.thing", object: thing) + +try binder.serve() // blocks, dispatching incoming calls +``` + +`transaction.arguments(interface:)` reads and checks the interface token, +returning a reader at the first argument; `Parcel.reply` / `Parcel.exception` +write the reply's exception header for you. + +Register before serving: a caller that finds the service first simply blocks +until the loop starts, rather than failing. Only `transaction.senderProcessID` +and `senderUserID` are trustworthy for a permission check — the driver fills them +in; everything in the request parcel is whatever the caller chose to write. + +## Death notifications + +The only race-free way to learn a peer has gone — a handle never becomes +detectably invalid on its own, and the driver may reissue a dead handle's number +for a different object. + +```swift +let watch = try binder.notifyOnDeath(of: activity.object) { + // runs on a serving thread +} +try binder.cancel(watch) // confirmed by the driver, not retired locally +``` + +## Design + +| Type | Role | +|---|---| +| `BinderConnection` | an open, mapped device; the entry point for everything | +| `Parcel` | the serialised payload — read and write, with object offsets tracked | +| `RemoteObject` / `RemoteProxy` | a handle, and an owning strong reference to one | +| `LocalObject` | an object this process hosts for others to call | +| `ServiceManager` | the context manager at handle 0 | +| `DeathNotification` | a standing watch on a remote object's process | +| `CommandStream` / `ReturnStream` | the `BC_*` / `BR_*` codec under `BINDER_WRITE_READ` | + +The layering is deliberate. `Binder` is a bare open device — enough to read the +protocol version, nothing more. `BinderConnection` is the usable thing: it maps +the receive buffer, checks the version, and refuses to continue on a mismatch, +in the order `libbinder`'s `ProcessState` does. Deciding what an incoming command +requires (`action(for:)`) is kept separate from sending the response, so the part +where a serving loop actually goes wrong is a pure, testable function. + +## Building + +```sh +swift build +swift test +``` + +Requires Swift 6.0+. The two tests that need `/dev/binder` skip when it is +absent, so the suite passes on any host; the rest exercise the protocol layer +directly. + +## Not yet implemented + +- Arrays of file descriptors (`BINDER_TYPE_FDA`). Single descriptors + (`BINDER_TYPE_FD`) and scatter-gather buffers, including nested ones, are + supported. +- The freeze and node-debug maintenance ioctls (`BINDER_FREEZE`, + `BINDER_GET_NODE_DEBUG_INFO`, `BINDER_GET_NODE_INFO_FOR_REF`). +- `serve()` cannot interrupt itself while parked in the driver; closing the + connection is what ends a quiet loop. diff --git a/Sources/Binder/BinderConnection.swift b/Sources/Binder/BinderConnection.swift new file mode 100644 index 0000000..38eddc7 --- /dev/null +++ b/Sources/Binder/BinderConnection.swift @@ -0,0 +1,154 @@ +// +// BinderConnection.swift +// Binder +// + +import Dispatch +import Synchronization +import SystemPackage +import Socket +import CBinder + +/// A binder device that is ready to carry transactions. +/// +/// Separate from ``Binder`` because opening the device and being able to *use* +/// it are different things. A bare ``Binder`` is an open file descriptor: enough +/// to ask the driver its protocol version, and nothing else. Transacting also +/// needs a mapped receive buffer, a protocol version that matches, and a +/// thread-pool bound the driver has been told about. Those are what this type +/// establishes, in the order libbinder's `ProcessState` establishes them: +/// +/// 1. open the device +/// 2. read `BINDER_VERSION` and refuse to continue on a mismatch +/// 3. `BINDER_SET_MAX_THREADS` +/// 4. `mmap` the receive buffer +/// +/// Keeping the two apart means ``BinderVersion/read(_:)`` does not have to map +/// a megabyte of address space to answer one ioctl. +/// +/// ## Why a class +/// +/// A connection is shared, not copied: several tasks transact over one device, +/// and the driver is built for exactly that — concurrent ioctls from different +/// threads are the normal case, which is what its thread pool is for. Reference +/// semantics also make the resources' lifetime unambiguous, since the file +/// descriptor and the mapping are released when the last user lets go. +/// +/// It has to be a class for a second reason: ``transact(handle:code:request:flags:)`` +/// blocks, so the async form has to hand the work to another thread through an +/// escaping closure, and a non-copyable value cannot be captured by one. +public final class BinderConnection { + + /// The open device. + internal let device: Binder + + /// The region the driver writes incoming transactions into. + /// + /// Never read directly by this type. It exists to be kept alive: the + /// pointers a transaction hands back point into it, and unmapping it while + /// a reply is outstanding invalidates them. + internal let buffer: MappedBuffer + + /// Where blocking ioctls run when called from an async context. + /// + /// Concurrent, not serial: two transactions to different services have no + /// reason to queue behind one another, and a synchronous binder call parks + /// its thread until the peer answers — so serialising here would let one + /// slow service stall every other call on the connection. + internal let queue: DispatchQueue + + /// The objects this connection hosts for other processes to call. + internal let localObjects = LocalObjectRegistry() + + /// Standing requests to be told when a remote object's process dies. + internal let deaths = DeathRegistry() + + /// Whether a serving loop should keep going. + /// + /// Read by every serving thread and written by ``stopServing()``, which is + /// called from elsewhere by definition — a loop parked in an ioctl cannot + /// stop itself. + internal let isServing = Mutex(false) + + /// The receive-buffer size libbinder maps, and therefore the size services + /// on the other end are provisioned to expect. + /// + /// One megabyte less two pages — the kernel accounts its own overhead + /// inside the mapping, so a round megabyte yields less usable space than + /// callers assume. + public static var defaultBufferSize: Int { + MappedBuffer.defaultSize + } + + /// Open a binder device and prepare it for transactions. + /// + /// - Parameters: + /// - path: the device to open, `/dev/binder` by default. Android also + /// exposes `/dev/hwbinder` and `/dev/vndbinder`, which speak the same + /// protocol on separate contexts. + /// - maxThreads: how many threads the driver may ask this process to + /// spawn to service incoming transactions. + /// - bufferSize: bytes of receive buffer to map. + /// + /// - Throws: `Errno.protocolNotSupported` if the driver's protocol version + /// differs from the one these headers were built against. Continuing + /// past that would put mismatched structures on the wire, so it is + /// refused here rather than failing confusingly at the first transaction. + public init( + path: String = Binder.path, + maxThreads: UInt32 = Binder.defaultMaxThreads, + bufferSize: Int = BinderConnection.defaultBufferSize + ) throws(Errno) { + let device = try Binder(path: path, mode: .readWrite) + + let version = try device.version + guard version == .compiledVersion else { + throw .protocolNotSupported + } + + // Advisory: the driver logs and carries on if it refuses this, and so + // does libbinder. A process that cannot raise the bound still works, + // it just serialises incoming work. + try? device.setMaxThreads(maxThreads) + + let buffer = try MappedBuffer.map( + FileDescriptor(rawValue: device.handle.fileDescriptor.rawValue), + size: bufferSize + ) + + self.device = device + self.buffer = buffer + self.queue = DispatchQueue( + label: "org.pureswift.Binder.\(path)", + qos: .userInitiated, + attributes: .concurrent + ) + } +} + +// The stored properties are established at init and never mutated: a file +// descriptor, a mapping's address and length, and a queue. Everything a +// transaction touches beyond those is local to the call. The driver is designed +// for concurrent ioctls from several threads, so sharing a connection across +// tasks is the intended use rather than something to be guarded against. +extension BinderConnection: @unchecked Sendable {} + +public extension BinderConnection { + + /// The protocol version the driver reported when this connection opened. + /// + /// Always equal to ``BinderVersion/compiledVersion``; a mismatch would have + /// prevented the connection from being created. + var version: BinderVersion { + .compiledVersion + } + + /// The size of the mapped receive buffer. + /// + /// The upper bound on a single incoming transaction's payload. A service + /// sending more than this gets a failed transaction, so it is worth knowing + /// when sizing bulk transfers. + var bufferSize: Int { + buffer.size + } +} diff --git a/Sources/Binder/BinderError.swift b/Sources/Binder/BinderError.swift new file mode 100644 index 0000000..5ee01b7 --- /dev/null +++ b/Sources/Binder/BinderError.swift @@ -0,0 +1,66 @@ +// +// BinderError.swift +// Binder +// + +import SystemPackage + +/// Why a binder operation did not complete. +/// +/// Deliberately not a flat list of errno values. A transaction can fail in ways +/// the file-descriptor layer has no vocabulary for — the peer process died, the +/// driver refused the transaction, the reply did not decode — and collapsing +/// those into `Errno` would leave a caller unable to tell "retry later" from +/// "this object is gone forever". +public enum BinderError: Error, Equatable, Hashable, Sendable { + + /// An ioctl or other system call failed. + case system(Errno) + + /// The reply could not be decoded. + case parcel(ParcelError) + + /// The driver's return stream could not be walked. + case stream(ReturnStreamError) + + /// `BR_DEAD_REPLY`: the process hosting the target is gone. + /// + /// Permanent for this handle. Retrying is pointless; the caller has to + /// look the service up again. + case deadObject + + /// `BR_FAILED_REPLY`: the driver rejected the transaction. + /// + /// Usually a malformed parcel, an unknown handle, or a payload larger than + /// the receiver's mapped buffer. + case failedTransaction + + /// `BR_FROZEN_REPLY`: the target process is frozen and cannot be reached. + /// + /// Unlike ``deadObject`` this is temporary — the process may thaw. + case frozen + + /// `BR_ERROR`, carrying the driver's own status value. + case driver(Int32) + + /// A return command arrived that has no meaning at this point in the + /// exchange. + /// + /// Not folded into a generic failure because it means this code and the + /// driver disagree about the protocol, which is a bug rather than a + /// runtime condition. + case unexpectedReturn(DriverReturnProtocol) + + /// The driver reported a reply larger than the buffer it was mapped into. + /// + /// Would mean reading outside the mapping, so it is refused rather than + /// clamped. + case replyOutOfBounds + + /// The callee threw, and the reply carries its exception code rather than + /// the declared return value. + /// + /// A protocol-level outcome rather than a transport failure: the + /// transaction itself succeeded. + case remoteException(Int32) +} diff --git a/Sources/Binder/BufferObject.swift b/Sources/Binder/BufferObject.swift new file mode 100644 index 0000000..700d6ee --- /dev/null +++ b/Sources/Binder/BufferObject.swift @@ -0,0 +1,163 @@ +// +// BufferObject.swift +// Binder +// + +import CBinder + +/// Out-of-line buffers, the scatter-gather half of the protocol. +/// +/// Most data goes inline in the parcel, copied as one block. A *buffer object* +/// is different: the parcel holds only a `binder_buffer_object` descriptor — +/// a pointer and a length — and the bytes it names live somewhere else. At send +/// time the driver copies each such region into a separate area of the +/// receiver's mapped space and rewrites the descriptor's pointer to land there. +/// +/// This is how HIDL passes anything with its own storage: a `hidl_vec`, a +/// `hidl_string`, a fixed-size array behind a pointer. The vector's elements are +/// a buffer object; the struct that refers to them is inline and carries the +/// offset back to it. Waydroid reaches this whenever it talks to a hardware +/// (`@1.0::IFoo`) service rather than a plain AIDL one. +/// +/// ## Why the payload has to be retained +/// +/// The descriptor's pointer must be a valid address for the whole ioctl, +/// exactly like the parcel's own bytes. But unlike the inline data it points +/// *outside* the parcel, so the parcel has to keep the payload alive itself and +/// the real address cannot be written until send time — the bytes may move +/// before then. So ``append(buffer:)`` stores the payload and a placeholder +/// pointer, and the transaction path fills the pointer in once the payload is +/// staged at a fixed address. +public extension Parcel { + + /// A flag on a buffer object, mirroring `BINDER_BUFFER_FLAG_HAS_PARENT`. + private static var bufferHasParent: UInt32 { 0x01 } + + /// Round up to an 8-byte boundary. + /// + /// Buffer payloads are copied into the receiver at pointer alignment, and + /// `buffers_size` — the total the driver reserves — is the sum of the + /// *aligned* lengths, not the raw ones. Reserving the raw sum would leave + /// the driver short for all but the last buffer. + internal static func bufferAlignedSize(_ length: Int) -> Int { + (length + 7) & ~7 + } + + /// Append a top-level out-of-line buffer. + /// + /// The bytes are copied into the parcel's own keeping and sent as a + /// separate region the receiver reads through the buffer object's offset. + /// The descriptor is recorded in ``objectOffsets`` like any object, so the + /// driver processes it; the payload is recorded separately so the transaction + /// path can stage it and fix up the pointer. + /// + /// - Returns: the index of this buffer among the parcel's objects, for use + /// as the `parent` of a nested buffer. + @discardableResult + mutating func append(buffer bytes: [UInt8]) -> Int { + appendBufferObject(bytes, parent: nil, parentOffset: 0) + } + + /// Append a buffer whose pointer lives inside another buffer. + /// + /// This is how nested HIDL data is laid out: a `hidl_vec` is a + /// top-level buffer of `hidl_string` structs, and each string's characters + /// are a child buffer whose pointer field sits *inside* that top-level + /// buffer. The driver relocates the child, then writes its new address into + /// the parent at `parentOffset` — so the parent's payload carries a + /// placeholder there, which the caller leaves room for. + /// + /// - Parameters: + /// - bytes: the child payload. + /// - parent: the object index returned by the parent's ``append(buffer:)``. + /// Must refer to a buffer appended earlier; the driver rejects a parent + /// index that is not a prior buffer object. + /// - parentOffset: the byte offset within the parent's payload where the + /// child's pointer belongs. Must leave room for an 8-byte pointer inside + /// the parent. + mutating func append(buffer bytes: [UInt8], parent: Int, parentOffset: Int) { + _ = appendBufferObject(bytes, parent: parent, parentOffset: parentOffset) + } + + private mutating func appendBufferObject( + _ bytes: [UInt8], + parent: Int?, + parentOffset: Int + ) -> Int { + // The index this object will occupy in the offsets array — what a child + // names as its parent. + let index = objectOffsets.count + + var object = binder_buffer_object() + object.hdr = binder_object_header(type: .pointer) + // Placeholder: the real address is written at send time, once the + // payload has been staged somewhere that will not move. + object.buffer = 0 + object.length = binder_size_t(bytes.count) + if let parent { + object.flags = Parcel.bufferHasParent + object.parent = binder_size_t(parent) + object.parent_offset = binder_size_t(parentOffset) + } else { + object.flags = 0 + object.parent = 0 + object.parent_offset = 0 + } + + // Recorded in both: the descriptor is an object the driver processes + // (so it goes in the offsets array), and its payload is out of line (so + // the transaction path can stage it and fix up the pointer). Missing + // the object offset would leave the driver never looking at the buffer + // at all. + recordBuffer(at: self.bytes.count, payload: bytes) + recordObjectOffset() + append(bitPattern: object) + return index + } + + /// Whether this parcel carries any out-of-line buffers, and so must be sent + /// with `BC_TRANSACTION_SG` rather than `BC_TRANSACTION`. + var hasBuffers: Bool { + !scatterGatherBuffers.isEmpty + } + + /// The total space the driver must reserve for this parcel's buffers. + /// + /// The sum of each payload's 8-aligned length, which is what + /// `binder_transaction_data_sg.buffers_size` carries. + var buffersSize: Int { + scatterGatherBuffers.reduce(0) { $0 + Parcel.bufferAlignedSize($1.payload.count) } + } +} + +public extension Parcel.Reader { + + /// Read the payload of an out-of-line buffer. + /// + /// The descriptor sits inline at the cursor; the bytes it names were copied + /// out of the mapping when the reply was received, so this returns them + /// directly rather than following the descriptor's pointer — which by now + /// refers to freed shared memory. + /// + /// - Returns: the payload, or `nil` if the object at the cursor is not a + /// buffer, or if its bytes were not captured (a buffer written locally + /// rather than received carries its payload out of band, not here). + mutating func readBuffer(from parcel: Parcel) throws(ParcelError) -> [UInt8]? { + // Peek the object header before committing to the 40-byte read. A + // different object at the cursor is shorter than a buffer descriptor, + // so reading blindly would either overrun the next value or fail with + // endOfParcel on a short parcel. The type decides how much to consume. + let position = offset + guard position + MemoryLayout.size <= bytes.count else { + throw ParcelError.endOfParcel + } + let type = bytes[position ..< position + MemoryLayout.size] + .withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } + guard type == BinderType.pointer.rawValue else { + return nil + } + + _ = try read(count: MemoryLayout.size) + return parcel.scatterGatherBuffers.first { $0.offset == position }?.payload + } +} diff --git a/Sources/Binder/Call.swift b/Sources/Binder/Call.swift new file mode 100644 index 0000000..855af0d --- /dev/null +++ b/Sources/Binder/Call.swift @@ -0,0 +1,189 @@ +// +// Call.swift +// Binder +// + +/// Making and answering AIDL calls without repeating the envelope. +/// +/// Every AIDL method call has the same shape around its arguments: write the +/// interface token, transact, read the reply's exception header, and only then +/// read the return values. Every method that skips or misorders any of that +/// fails in the same hard-to-place way — the callee rejects a request whose +/// token is missing, and a client that forgets to consume the exception header +/// reads it as the first field of the result. These helpers write that envelope +/// once so a caller supplies only what differs: the transaction code and the +/// arguments. +public extension RemoteProxy { + + /// Call a method and return a reader positioned at the first result. + /// + /// Writes the interface token, appends the caller's arguments, transacts, + /// and consumes the reply's exception header — throwing + /// ``BinderError/remoteException(_:)`` if it is non-zero. The returned + /// reader is positioned exactly where the return values begin. + /// + /// - Parameters: + /// - code: the transaction code, offset from `FIRST_CALL_TRANSACTION`. + /// - interface: the descriptor the callee will check the token against. + /// - header: which build the callee belongs to; ``RPCHeader/system`` for + /// an Android container. + /// - arguments: appends the method's arguments, in declaration order. + func call( + _ code: UInt32, + interface: String, + header: RPCHeader = .system, + _ arguments: (inout Parcel) -> Void = { _ in } + ) throws(BinderError) -> Parcel.Reader { + var request = Parcel() + request.appendInterfaceToken(interface, header: header) + arguments(&request) + + let reply = try transact(code: code, request: request) + + var reader = reply.reader() + let exception: Int32 + do { + exception = try reader.readInt32() + } catch { + throw BinderError.parcel(error) + } + guard exception == 0 else { + throw BinderError.remoteException(exception) + } + return reader + } + + /// Call a one-way method, which returns nothing. + /// + /// A one-way call carries no reply and therefore no exception header: the + /// driver returns as soon as it has taken the transaction, and any failure + /// on the callee's side is unobservable here by design. Do not use it where + /// a result — or confirmation — is needed. + func post( + _ code: UInt32, + interface: String, + header: RPCHeader = .system, + _ arguments: (inout Parcel) -> Void = { _ in } + ) throws(BinderError) { + var request = Parcel() + request.appendInterfaceToken(interface, header: header) + arguments(&request) + + _ = try transact(code: code, request: request, flags: [.acceptFds, .oneWay]) + } +} + +// MARK: - Async + +public extension RemoteProxy { + + /// Call a method and suspend until the reply arrives. + /// + /// The async counterpart of ``call(_:interface:header:_:)``. A `Parcel` + /// value is returned rather than a `Reader` because a reader is a + /// non-`Sendable` cursor; the caller opens one on the result. The exception + /// header is already consumed, so the returned parcel's reader starts at the + /// return values via ``resultReader()``. + func call( + _ code: UInt32, + interface: String, + header: RPCHeader = .system, + _ arguments: @Sendable (inout Parcel) -> Void = { _ in } + ) async throws(BinderError) -> Parcel { + var request = Parcel() + request.appendInterfaceToken(interface, header: header) + arguments(&request) + + let reply = try await transact(code: code, request: request) + + var reader = reply.reader() + let exception: Int32 + do { + exception = try reader.readInt32() + } catch { + throw BinderError.parcel(error) + } + guard exception == 0 else { + throw BinderError.remoteException(exception) + } + // The header is validated but the parcel is returned whole; the caller + // reads results from resultReader(), which skips it again. Returning the + // reader itself is not possible across the suspension because it is not + // Sendable. + return reply + } +} + +public extension Parcel { + + /// A reader positioned just past a reply's exception header. + /// + /// For use on the parcel returned by the async ``RemoteProxy/call(_:interface:header:_:)``, + /// whose header has already been checked. Skips the leading `Int32` so the + /// cursor starts at the return values. + func resultReader() -> Reader { + var reader = self.reader() + _ = try? reader.readInt32() + return reader + } +} + +// MARK: - Serving + +public extension LocalObject.Transaction { + + /// Read and check the interface token at the front of this request. + /// + /// The counterpart of ``RemoteProxy/call(_:interface:header:_:)`` on the + /// serving side: a handler calls this first to confirm the caller named the + /// interface it implements, and receives a reader positioned at the + /// arguments. + /// + /// - Returns: a reader positioned at the first argument. + /// - Throws: ``ParcelError/interfaceMismatch(expected:found:)`` if the + /// caller's build marker differs, ``ParcelError/wrongInterface(expected:found:)`` + /// if the descriptor names a different interface, or + /// ``ParcelError/endOfParcel`` if the request has no token — any of which + /// a handler should answer as a failed transaction rather than proceeding + /// to read arguments that are not there. + func arguments( + interface: String, + header: RPCHeader = .system + ) throws(ParcelError) -> Parcel.Reader { + var reader = request.reader() + let descriptor = try reader.readInterfaceToken(header: header) + guard descriptor == interface else { + throw ParcelError.wrongInterface(expected: interface, found: descriptor) + } + return reader + } +} + +// MARK: - Building a reply + +public extension Parcel { + + /// A reply that carries a successful result. + /// + /// Writes the `EX_NONE` exception header every AIDL reply opens with, then + /// runs the caller's closure to append the return values. A handler that + /// returns a bare parcel without the header sends its first return value + /// where the caller expects the exception, so this keeps them from being + /// separated. + static func reply(_ results: (inout Parcel) -> Void = { _ in }) -> Parcel { + var parcel = Parcel() + parcel.append(Int32(0)) + results(&parcel) + return parcel + } + + /// A reply that reports a service-specific error. + /// + /// The negative of a Binder exception code — `EX_SERVICE_SPECIFIC` and the + /// like. A caller sees it as ``BinderError/remoteException(_:)``. + static func exception(_ code: Int32) -> Parcel { + var parcel = Parcel() + parcel.append(code) + return parcel + } +} diff --git a/Sources/Binder/CommandStream.swift b/Sources/Binder/CommandStream.swift new file mode 100644 index 0000000..3980405 --- /dev/null +++ b/Sources/Binder/CommandStream.swift @@ -0,0 +1,90 @@ +// +// CommandStream.swift +// Binder +// + +import CBinder + +/// The size of a command's payload, taken from the command code itself. +/// +/// `BC_*` and `BR_*` are not small integers: each is a full `_IOC` value, and +/// the size of the structure that follows it in the stream is encoded in bits +/// 16-29 of that value by the same macros the kernel uses. Reading the size +/// back out means a payload length can never disagree with the kernel's idea of +/// it, which a hand-written table of sizes eventually would. +internal protocol DriverProtocolCommand { + var rawValue: UInt32 { get } +} + +internal extension DriverProtocolCommand { + + var payloadSize: Int { + #if canImport(Darwin) + // sys/ioccom.h uses a 13-bit size field; linux/ioctl.h uses 14. Binder + // is a Linux driver, so this arm only matters to the macOS mock build. + return Int((rawValue >> 16) & 0x1fff) + #else + return Int((rawValue >> 16) & 0x3fff) + #endif + } +} + +extension DriverCommandProtocol: DriverProtocolCommand {} +extension DriverReturnProtocol: DriverProtocolCommand {} + +/// A sequence of `BC_*` commands bound for the driver's write buffer. +/// +/// A `BINDER_WRITE_READ` ioctl does not carry one command. It carries a byte +/// stream of them: each a 32-bit code, immediately followed by that command's +/// payload with no padding and no framing. The driver consumes as many as it +/// can and reports back how many bytes it took, which is why +/// ``WriteReadTransaction`` has a `write_consumed` field separate from +/// `write_size`. +public struct CommandStream: Equatable, Hashable, Sendable { + + /// The encoded stream. + public private(set) var bytes: [UInt8] + + public init() { + self.bytes = [] + } + + internal init(bytes: [UInt8]) { + self.bytes = bytes + } + + public var isEmpty: Bool { + bytes.isEmpty + } + + public var count: Int { + bytes.count + } +} + +public extension CommandStream { + + /// Append a command that carries no payload, such as `BC_ENTER_LOOPER`. + mutating func append(_ command: DriverCommandProtocol) { + assert(command.payloadSize == 0, "\(command) carries a payload") + appendRaw(command.rawValue) + } + + /// Append a command and the structure that belongs to it. + /// + /// The payload goes in verbatim, in host layout. These structures are + /// shared with the kernel by pointer elsewhere in the same protocol, so + /// their in-memory form *is* the wire form. + mutating func append(_ command: DriverCommandProtocol, payload: Payload) { + assert( + command.payloadSize == MemoryLayout.size, + "\(command) expects \(command.payloadSize) bytes, got \(MemoryLayout.size)" + ) + appendRaw(command.rawValue) + withUnsafeBytes(of: payload) { bytes.append(contentsOf: $0) } + } + + private mutating func appendRaw(_ value: UInt32) { + withUnsafeBytes(of: value) { bytes.append(contentsOf: $0) } + } +} diff --git a/Sources/Binder/ContextManager.swift b/Sources/Binder/ContextManager.swift new file mode 100644 index 0000000..c15c825 --- /dev/null +++ b/Sources/Binder/ContextManager.swift @@ -0,0 +1,102 @@ +// +// ContextManager.swift +// Binder +// + +import SystemPackage +import Socket +import CBinder + +internal extension Binder { + + /// The `BINDER_SET_CONTEXT_MGR` payload — a legacy `__s32` the driver reads + /// but does not use for anything a caller controls. + struct SetContextManager { + + var value: Int32 = 0 + } +} + +extension Binder.SetContextManager: IOControlValue { + + static var id: BinderCommand { .setContextMgr } + + mutating func withUnsafeMutablePointer( + _ body: (UnsafeMutableRawPointer) throws -> (Result) + ) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &self) { buffer in + try body(buffer.baseAddress!) + } + } +} + +extension flat_binder_object: @retroactive IOControlValue { + + /// Used only as the `BINDER_SET_CONTEXT_MGR_EXT` payload; the same structure + /// travels inline in parcels, where it is not an ioctl value. + public static var id: BinderCommand { .setContextMgrExt } + + public mutating func withUnsafeMutablePointer( + _ body: (UnsafeMutableRawPointer) throws -> (Result) + ) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &self) { buffer in + try body(buffer.baseAddress!) + } + } +} + +public extension BinderConnection { + + /// Register this process as the context manager — handle zero. + /// + /// There is exactly one context manager per binder context, and every other + /// process reaches it at handle zero without being told where it is. Calling + /// this makes *this* process that endpoint: transactions addressed to handle + /// zero are delivered here, so a lookup service — a `servicemanager` — is + /// built by becoming the context manager and then serving. + /// + /// It fails if a context manager already exists for this context, which is + /// the usual case on a running Android system. Reserve it for a context you + /// own: a fresh `binderfs` instance, or a container whose manager has not + /// started. + /// + /// - Parameter securityContext: when true, ask the driver to deliver + /// incoming transactions as `BR_TRANSACTION_SEC_CTX` — carrying the + /// caller's SELinux context — via `BINDER_SET_CONTEXT_MGR_EXT`. A plain + /// registration is used otherwise, which is the widely compatible path. + /// + /// Nothing is delivered until a serving loop runs; see ``serve(while:)``. + func becomeContextManager(securityContext: Bool = false) throws(BinderError) { + if securityContext { + var object = flat_binder_object() + object.hdr = binder_object_header(type: .binder) + // The manager registers its own node; the security-context flag is + // what asks the driver for BR_TRANSACTION_SEC_CTX delivery. + object.flags = FlatBinderObjectFlags.txnSecurityCtx.rawValue + object.binder = 0 + object.cookie = 0 + #if ENABLE_MOCKING + assert(type(of: object).id.rawValue == BINDER_SET_CONTEXT_MGR_EXT) + #else + do { + try device.handle.fileDescriptor.inputOutput(&object) + } catch { + throw BinderError.system(error) + } + #endif + } else { + var value = Binder.SetContextManager() + #if ENABLE_MOCKING + assert(type(of: value).id.rawValue == BINDER_SET_CONTEXT_MGR) + #else + // do/catch inside the real branch: under mocking nothing throws, so + // a catch would infer `any Error` rather than Errno. + do { + try device.handle.fileDescriptor.inputOutput(&value) + } catch { + throw BinderError.system(error) + } + #endif + } + } +} diff --git a/Sources/Binder/DeathNotification.swift b/Sources/Binder/DeathNotification.swift new file mode 100644 index 0000000..1832de3 --- /dev/null +++ b/Sources/Binder/DeathNotification.swift @@ -0,0 +1,173 @@ +// +// DeathNotification.swift +// Binder +// + +import Synchronization +import CBinder + +/// A standing request to be told when a remote object's process dies. +/// +/// The only way to learn that a peer has gone. A handle does not become +/// detectably invalid — transactions to a dead object fail, but nothing arrives +/// to say so until one is attempted, and a client that is merely holding a +/// reference would never find out. Worse, the driver may reissue the handle +/// number for a different object once the reference count drops, so polling by +/// pinging is not equivalent. +/// +/// Registration is per `(object, cookie)` pair, which is why this is a distinct +/// object rather than a closure on ``RemoteObject``: the same remote object can +/// be watched several times independently, and cancelling one watch must not +/// cancel the others. +public final class DeathNotification: Sendable { + + /// The object being watched. + public let object: RemoteObject + + /// What the driver hands back to identify this registration. + internal let cookie: UInt64 + + internal let handler: @Sendable () -> Void + + internal init( + object: RemoteObject, + cookie: UInt64, + handler: @escaping @Sendable () -> Void + ) { + self.object = object + self.cookie = cookie + self.handler = handler + } +} + +/// The death notifications a connection has registered, keyed by cookie. +internal final class DeathRegistry: Sendable { + + private let state = Mutex(State()) + + private struct State { + var notifications: [UInt64: DeathNotification] = [:] + /// Cookies start at one so that zero stays available as "no cookie". + var nextCookie: UInt64 = 1 + } + + func register( + object: RemoteObject, + handler: @escaping @Sendable () -> Void + ) -> DeathNotification { + state.withLock { state in + let cookie = state.nextCookie + state.nextCookie += 1 + let notification = DeathNotification( + object: object, + cookie: cookie, + handler: handler + ) + state.notifications[cookie] = notification + return notification + } + } + + func notification(for cookie: UInt64) -> DeathNotification? { + state.withLock { $0.notifications[cookie] } + } + + func remove(cookie: UInt64) -> DeathNotification? { + state.withLock { $0.notifications.removeValue(forKey: cookie) } + } + + var count: Int { + state.withLock { $0.notifications.count } + } +} + +// MARK: - Registration + +public extension BinderConnection { + + /// Ask to be told when a remote object's hosting process dies. + /// + /// - Parameters: + /// - object: the object to watch. A reference to it must be held for the + /// watch to mean anything; watching a handle whose count has reached + /// zero watches whatever the driver reissues that number to. + /// - handler: runs on a serving thread. Nothing is delivered unless a + /// serving loop is running — see ``serve(while:)`` — because the + /// notification arrives as a command in the return stream like any + /// other. + /// + /// - Returns: the registration, needed to cancel it. + func notifyOnDeath( + of object: RemoteObject, + handler: @escaping @Sendable () -> Void + ) throws(BinderError) -> DeathNotification { + let notification = deaths.register(object: object, handler: handler) + + var payload = binder_handle_cookie() + payload.handle = object.handle + payload.cookie = binder_uintptr_t(notification.cookie) + + var commands = CommandStream() + commands.append(.requestDeathNotification, payload: payload) + try send(commands) + + return notification + } + + /// Cancel a death notification. + /// + /// The registration is not forgotten here. The driver confirms with + /// `BR_CLEAR_DEATH_NOTIFICATION_DONE`, and a death already on its way can + /// still arrive first; dropping the record now would leave that arrival + /// unattributable, and the driver waiting on a `BC_DEAD_BINDER_DONE` that + /// never comes. + func cancel(_ notification: DeathNotification) throws(BinderError) { + var payload = binder_handle_cookie() + payload.handle = notification.object.handle + payload.cookie = binder_uintptr_t(notification.cookie) + + var commands = CommandStream() + commands.append(.clearDeathNotification, payload: payload) + try send(commands) + } +} + +// MARK: - Liveness + +public extension BinderConnection { + + /// The transaction code every binder object answers. + /// + /// `IBinder::PING_TRANSACTION`, which is the four characters `_PNG` packed + /// into an integer rather than a small number — transaction codes are + /// interface-defined, so the universal ones are placed far away from the + /// range an interface would use. + static var pingTransaction: UInt32 { 0x5F50_4E47 } + + /// Ask an object whether it is still there. + /// + /// - Returns: `true` if it answered. + /// + /// A negative answer is only a snapshot: the peer can die immediately + /// afterwards, so this cannot be built into a reliable liveness check. + /// ``notifyOnDeath(of:handler:)`` is the mechanism that does not race. + func ping(_ object: RemoteObject) throws(BinderError) -> Bool { + do { + _ = try transact(with: object, code: Self.pingTransaction, request: Parcel()) + return true + } catch .deadObject { + return false + } catch .failedTransaction { + return false + } + } + + /// Whether the context manager is answering. + /// + /// Worth checking before a lookup: on a system where the service manager + /// has not started, ``service(named:)`` blocks in the driver for five + /// seconds per call before giving up, which reads as a hang. + var isServiceManagerPresent: Bool { + (try? ping(.contextManager)) ?? false + } +} diff --git a/Sources/Binder/ExtendedError.swift b/Sources/Binder/ExtendedError.swift new file mode 100644 index 0000000..5c3e50a --- /dev/null +++ b/Sources/Binder/ExtendedError.swift @@ -0,0 +1,77 @@ +// +// ExtendedError.swift +// Binder +// + +import SystemPackage +import Socket +import CBinder + +/// The driver's own account of why the last transaction on this thread failed. +/// +/// A failed transaction surfaces as `BR_FAILED_REPLY` — a bare command with no +/// detail. The reason lives in per-thread state the driver keeps, and this ioctl +/// reads it: the command that failed, and a status the driver would otherwise +/// only have logged. It is the difference between "the transaction failed" and +/// "the transaction failed because the parcel was too large for the receiver's +/// buffer". +/// +/// Thread-scoped, and it reports the *most recent* failure on the calling +/// thread. Read it on the same thread that saw the failure, before doing +/// anything else that transacts, or it describes a later call instead. +public typealias ExtendedError = binder_extended_error + +extension ExtendedError: @retroactive IOControlValue { + + public static var id: BinderCommand { .getExtendedError } + + public mutating func withUnsafeMutablePointer( + _ body: (UnsafeMutableRawPointer) throws -> (Result) + ) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &self) { buffer in + try body(buffer.baseAddress!) + } + } +} + +public extension ExtendedError { + + /// The command whose failure this describes, as a `BR_*` return code. + /// + /// The stored `command` field is the raw `__u32`; this is the typed view of + /// it. + var failedCommand: DriverReturnProtocol { + DriverReturnProtocol(rawValue: command) + } + + /// The driver's status for the failure. Interpreted as a `binder_status_t`, + /// most values are negated errnos; see ``BinderError``. + var status: Int32 { + param + } +} + +public extension BinderConnection { + + /// Ask the driver why the last transaction on this thread failed. + /// + /// Meaningful only straight after a failure, on the thread that saw it. The + /// initial state before any failure reports success, so a value read out of + /// order is misleading rather than merely stale. + func lastError() throws(BinderError) -> ExtendedError { + var value = ExtendedError() + #if ENABLE_MOCKING + assert(type(of: value).id.rawValue == BINDER_GET_EXTENDED_ERROR) + #else + // The do/catch lives inside the real branch on purpose: under mocking + // the block throws nothing, so a catch here would infer `any Error` and + // fail to convert to Errno. + do { + try device.handle.fileDescriptor.inputOutput(&value) + } catch { + throw BinderError.system(error) + } + #endif + return value + } +} diff --git a/Sources/Binder/FileDescriptorObject.swift b/Sources/Binder/FileDescriptorObject.swift new file mode 100644 index 0000000..bc28e2a --- /dev/null +++ b/Sources/Binder/FileDescriptorObject.swift @@ -0,0 +1,73 @@ +// +// FileDescriptorObject.swift +// Binder +// + +import SystemPackage +import CBinder + +/// Passing an open file descriptor through a transaction. +/// +/// A file descriptor is a number that means something only inside one process's +/// table, so it cannot simply be written as an integer — the same number names +/// a different file, or nothing, in the receiver. Binder handles this the way it +/// handles object handles: the descriptor travels as a `binder_fd_object` at a +/// declared offset, and the driver `dup`s it into the receiving process, +/// yielding a new number there that refers to the same open file. +/// +/// ## Ownership crosses +/// +/// The sender keeps its descriptor; the driver duplicates rather than moves. The +/// **receiver** owns the number it is given and must close it — the driver +/// installs it and then forgets it, so a receiver that reads an fd out of a +/// parcel and drops it leaks a descriptor in its own table, one per transaction, +/// until it runs out. This is the one place decoding a parcel has a side effect +/// the reader is responsible for cleaning up. +/// +/// ## Only if the transaction allows it +/// +/// The driver refuses to translate a descriptor unless the transaction carries +/// `TF_ACCEPT_FDS` and the receiving node was created accepting them. A parcel +/// with a descriptor sent without `acceptFds` fails as a whole, rather than +/// arriving with the fd silently dropped. +public extension Parcel { + + /// Append an open file descriptor for the receiver to inherit a copy of. + /// + /// The descriptor is not consumed: the caller still owns and must close its + /// own. What the receiver gets is a duplicate. + mutating func append(fileDescriptor: FileDescriptor) { + var object = binder_fd_object() + object.hdr = binder_object_header(type: .fileDescriptor) + object.pad_flags = 0 + object.fd = UInt32(bitPattern: fileDescriptor.rawValue) + object.cookie = 0 + + recordObjectOffset() + append(bitPattern: object) + } +} + +public extension Parcel.Reader { + + /// Read a file descriptor the driver installed into this process. + /// + /// - Returns: the descriptor, or `nil` if the object at the cursor is not a + /// file descriptor. + /// + /// - Important: the returned descriptor is **owned by the caller**. The + /// driver has already placed it in this process's table; nothing else + /// will close it. Read it exactly once and close it when done, or it + /// leaks. + mutating func readFileDescriptor() throws(ParcelError) -> FileDescriptor? { + let slice = try read(count: MemoryLayout.size) + let object = Array(slice).withUnsafeBytes { + $0.loadUnaligned(as: binder_fd_object.self) + } + + guard object.hdr.type == BinderType.fileDescriptor.rawValue else { + return nil + } + return FileDescriptor(rawValue: CInt(bitPattern: object.fd)) + } +} diff --git a/Sources/Binder/InterfaceToken.swift b/Sources/Binder/InterfaceToken.swift new file mode 100644 index 0000000..f871607 --- /dev/null +++ b/Sources/Binder/InterfaceToken.swift @@ -0,0 +1,121 @@ +// +// InterfaceToken.swift +// Binder +// + +/// The four-character marker that opens every AIDL request. +/// +/// The receiver compares this against its own build's value and rejects the +/// whole transaction on a mismatch — `Parcel::enforceInterface` logs "Mixing +/// copies of libbinder?" and returns false. So it is not decoration: send the +/// wrong one and every call fails identically, with a message that points at +/// library packaging rather than at the header. +/// +/// Which value is right depends on the process being *talked to*, not on where +/// this code is compiled. A client on desktop Linux driving services inside an +/// Android container wants ``system``, because that is what those services were +/// built with. An off-Android libbinder would send ``unknown``, which is why a +/// naive port of libbinder's own logic fails against a real Android service. +public enum RPCHeader: Int32, Equatable, Hashable, Sendable, CaseIterable { + + /// `SYST` — the Android system image. The default, and what a container's + /// services expect. + case system + + /// `VNDR` — a vendor (VNDK) build. + case vendor + + /// `RECO` — a recovery build. + case recovery + + /// `UNKN` — kernel binder outside Android. What libbinder itself sends when + /// built off-Android, and almost never what a real service accepts. + case unknown +} + +public extension RPCHeader { + + /// Pack four characters the way `B_PACK_CHARS` does. + private static func packed(_ a: UInt8, _ b: UInt8, _ c: UInt8, _ d: UInt8) -> Int32 { + Int32(bitPattern: + (UInt32(a) << 24) | (UInt32(b) << 16) | (UInt32(c) << 8) | UInt32(d) + ) + } + + /// The value written into the parcel. + var value: Int32 { + switch self { + case .system: + return Self.packed(0x53, 0x59, 0x53, 0x54) // 'S','Y','S','T' + case .vendor: + return Self.packed(0x56, 0x4E, 0x44, 0x52) // 'V','N','D','R' + case .recovery: + return Self.packed(0x52, 0x45, 0x43, 0x4F) // 'R','E','C','O' + case .unknown: + return Self.packed(0x55, 0x4E, 0x4B, 0x4E) // 'U','N','K','N' + } + } +} + +public extension Parcel { + + /// The work-source value meaning "not propagating a work source". + /// + /// `IPCThreadState::kUnsetWorkSource`. + static var unsetWorkSource: Int32 { -1 } + + /// Set unconditionally by `writeInterfaceToken`, so that a violation + /// gathered on the callee's side can be reported back to the caller. + /// + /// `STRICT_MODE_PENALTY_GATHER`, which is `1 << 31` and therefore negative + /// in the `Int32` the parcel carries. + static var strictModePenaltyGather: Int32 { Int32(bitPattern: 1 << 31) } + + /// Open a request with the header every AIDL interface expects. + /// + /// Four fields in a fixed order: strict-mode policy, work source, the + /// four-character marker, then the interface descriptor. A request that + /// omits any of them is rejected before the callee looks at its arguments, + /// which presents as a transaction failure rather than as a decode error + /// and is correspondingly hard to place. + /// + /// - Parameters: + /// - descriptor: the interface name, e.g. `android.os.IServiceManager`. + /// - header: which build the callee belongs to. + /// - strictModePolicy: forwarded to the callee's thread state. Zero is + /// right for a caller that is not enforcing a policy of its own. + /// - workSource: the uid to attribute the work to, or + /// ``unsetWorkSource``. + mutating func appendInterfaceToken( + _ descriptor: String, + header: RPCHeader = .system, + strictModePolicy: Int32 = 0, + workSource: Int32 = Parcel.unsetWorkSource + ) { + append(strictModePolicy | Parcel.strictModePenaltyGather) + append(workSource) + append(header.value) + append(descriptor) + } +} + +public extension Parcel.Reader { + + /// Read and check the header at the front of an incoming request. + /// + /// - Returns: the descriptor, if the marker matched. + /// - Throws: ``ParcelError/interfaceMismatch(expected:found:)`` when the + /// marker disagrees, which means the two sides were built against + /// different binder configurations. + mutating func readInterfaceToken( + header: RPCHeader = .system + ) throws(ParcelError) -> String? { + _ = try readInt32() // strict-mode policy + _ = try readInt32() // work source + let marker = try readInt32() + guard marker == header.value else { + throw ParcelError.interfaceMismatch(expected: header.value, found: marker) + } + return try readString() + } +} diff --git a/Sources/Binder/LocalObject.swift b/Sources/Binder/LocalObject.swift new file mode 100644 index 0000000..94e7226 --- /dev/null +++ b/Sources/Binder/LocalObject.swift @@ -0,0 +1,172 @@ +// +// LocalObject.swift +// Binder +// + +import Synchronization +import CBinder + +/// An object this process hosts, which other processes can call. +/// +/// The mirror of ``RemoteProxy``. A proxy is a reference to something living +/// elsewhere; this is the something, seen from the side that implements it. +/// +/// Identity is a number, not a pointer. The driver keys its node on the +/// `binder` field of the `flat_binder_object` that first introduced the object, +/// and hands that value back on every incoming transaction. libbinder puts a +/// real object pointer there; this uses an opaque id and a registry instead, +/// because a raw pointer into a Swift heap object would have to outlive +/// whatever ARC decided, and a transaction arriving after that is a use of +/// freed memory rather than a lookup that fails. +public final class LocalObject: Sendable { + + /// An incoming call. + public struct Transaction: Sendable { + + /// The interface-defined transaction code. + public let code: UInt32 + + /// The caller's arguments. + public let request: Parcel + + /// Flags the caller sent. `oneWay` means no reply is expected, and any + /// parcel returned from the handler is discarded. + public let flags: TransactionFlags + + /// The calling process, as the *driver* reports it. + /// + /// Not attacker-controlled: the kernel fills these in, so they are the + /// only trustworthy thing in an incoming transaction and the right + /// basis for a permission check. Everything in ``request`` is whatever + /// the caller chose to write. + public let senderProcessID: Int32 + + /// The calling process's effective uid, as the driver reports it. + public let senderUserID: UInt32 + } + + /// Handles an incoming call. + /// + /// Returning `nil` replies with an empty parcel. Throwing reports the error + /// to the caller as a failed transaction rather than unwinding into the + /// serving loop, which has no way to report it and could not continue. + public typealias Handler = @Sendable (Transaction) throws(BinderError) -> Parcel? + + /// The interface this object implements, as the caller must name it. + public let interfaceDescriptor: String + + /// The driver's key for this object. + internal let id: UInt64 + + internal let handler: Handler + + internal init(id: UInt64, interfaceDescriptor: String, handler: @escaping Handler) { + self.id = id + self.interfaceDescriptor = interfaceDescriptor + self.handler = handler + } +} + +// MARK: - Registry + +/// The objects a connection is hosting, keyed by the id the driver returns. +/// +/// Holds them strongly. An object registered with the driver must stay callable +/// until it is withdrawn, and the driver has no way to consult Swift's object +/// graph before delivering a transaction — so lifetime here is decided by +/// registration, not by whether anything else still refers to it. +internal final class LocalObjectRegistry: Sendable { + + private let state = Mutex(State()) + + private struct State { + var objects: [UInt64: LocalObject] = [:] + /// Ids start at one; the driver treats zero as a null binder. + var nextID: UInt64 = 1 + } + + func register(interfaceDescriptor: String, handler: @escaping LocalObject.Handler) -> LocalObject { + state.withLock { state in + let id = state.nextID + state.nextID += 1 + let object = LocalObject( + id: id, + interfaceDescriptor: interfaceDescriptor, + handler: handler + ) + state.objects[id] = object + return object + } + } + + func object(for id: UInt64) -> LocalObject? { + state.withLock { $0.objects[id] } + } + + func remove(_ object: LocalObject) { + state.withLock { _ = $0.objects.removeValue(forKey: object.id) } + } + + var count: Int { + state.withLock { $0.objects.count } + } +} + +// MARK: - Connection + +public extension BinderConnection { + + /// Create an object other processes can call, and start hosting it. + /// + /// The object is callable as soon as a reference to it reaches another + /// process — by being written into a parcel — and stays callable until + /// ``withdraw(_:)``. Nothing is delivered to it until a serving loop is + /// running; see ``serve(while:)``. + /// + /// - Parameters: + /// - interface: the descriptor callers will send in their interface + /// token. + /// - handler: runs on a serving thread, not the caller's. It may run + /// concurrently with itself if more than one thread is serving. + func makeLocalObject( + interface: String, + handler: @escaping LocalObject.Handler + ) -> LocalObject { + localObjects.register(interfaceDescriptor: interface, handler: handler) + } + + /// Stop hosting an object. + /// + /// Transactions already in flight still run. Ones arriving afterwards are + /// answered with a failed transaction, because the driver has no way to + /// know the object is gone until it tries to deliver to it. + func withdraw(_ object: LocalObject) { + localObjects.remove(object) + } +} + +// MARK: - Parcelling + +public extension Parcel { + + /// Append a reference to an object this process hosts. + /// + /// Written as `BINDER_TYPE_BINDER`, which is what tells the driver the + /// object lives *here*: it creates a node on first sight and hands the + /// receiving process a handle pointing back at it. Sending the same object + /// again reuses that node. + /// + /// The `flags` field carries the node's scheduling priority in its low bits + /// as well as `acceptsFDs`; zero priority means the node inherits the + /// caller's, which is what a plain service wants. + mutating func append(_ value: LocalObject) { + var object = flat_binder_object() + object.hdr = binder_object_header(type: .binder) + object.flags = FlatBinderObjectFlags.acceptsFDs.rawValue + object.binder = binder_uintptr_t(value.id) + object.cookie = binder_uintptr_t(value.id) + + recordObjectOffset() + append(bitPattern: object) + } +} diff --git a/Sources/Binder/Looper.swift b/Sources/Binder/Looper.swift new file mode 100644 index 0000000..88eb56a --- /dev/null +++ b/Sources/Binder/Looper.swift @@ -0,0 +1,358 @@ +// +// Looper.swift +// Binder +// + +import Synchronization +import CBinder + +/// What a command arriving from the driver requires in response. +/// +/// Separated from the sending so that the decision is testable on its own. What +/// goes wrong in a serving loop is almost never the ioctl — it is answering the +/// wrong command, or failing to answer one the driver is waiting on, and both +/// are visible here. +/// +/// Not `Equatable`: the payloads are C structs containing unions, which Swift +/// cannot synthesise a comparison for. Tests pattern-match instead. +internal enum IncomingAction { + + /// Reply to a synchronous transaction and release its buffer. + case reply(Parcel, to: binder_transaction_data) + + /// A one-way transaction: release the buffer, send nothing back. + /// + /// Replying to one is a protocol error — the caller is not waiting and the + /// driver has no transaction to match the reply against. + case free(binder_uintptr_t) + + /// The driver is taking a reference to one of our objects and waiting to be + /// told we have noted it. + /// + /// Not optional. The driver blocks the node's reference transition until + /// the matching `*_DONE` arrives, so skipping this stalls whoever is trying + /// to acquire the object. + case acknowledge(DriverCommandProtocol, binder_ptr_cookie) + + /// A watched object's process has died and its handler has run. + /// + /// The driver keeps the death recipient alive until `BC_DEAD_BINDER_DONE` + /// arrives, so this must go back even though nothing is waiting on it. + case deathAcknowledged(binder_uintptr_t) + + /// Bookkeeping that needs no response. + case ignore +} + +// MARK: - Deciding + +internal extension BinderConnection { + + /// Work out what a command from the driver requires, running any handler it + /// implies. + func action(for element: ReturnStream.Element) throws(BinderError) -> IncomingAction { + switch element.command { + case .transaction: + guard let data = element.decode(as: binder_transaction_data.self) else { + throw BinderError.unexpectedReturn(element.command) + } + return try dispatch(data) + + case .transactionSecCtx: + // Delivered instead of BR_TRANSACTION when the target node was + // registered with FLAG_TXN_SECURITY_CTX. The structure is a plain + // binder_transaction_data with a security-context pointer appended, + // and the transaction sits at offset zero, so the same dispatch + // handles it once the trailing pointer is dropped. The context + // string itself is not surfaced yet; sender_pid and sender_euid, + // which a permission check actually needs, are already in the + // transaction. + guard let data = element.decode(as: binder_transaction_data_secctx.self) else { + throw BinderError.unexpectedReturn(element.command) + } + return try dispatch(data.transaction_data) + + case .incRefs: + // BC_INCREFS_DONE answers BR_INCREFS. + guard let cookie = element.decode(as: binder_ptr_cookie.self) else { + throw BinderError.unexpectedReturn(element.command) + } + return .acknowledge(.incRefsDone, cookie) + + case .acquire: + guard let cookie = element.decode(as: binder_ptr_cookie.self) else { + throw BinderError.unexpectedReturn(element.command) + } + return .acknowledge(.acquireDone, cookie) + + case .release, .decRefs: + // The driver is dropping a reference it holds. Nothing is expected + // back; the object stays hosted until it is withdrawn. + return .ignore + + case .noop, .spawnLooper, .transactionComplete, .onewaySpamSuspect, + .finished, .ok: + return .ignore + + case .deadBinder: + guard let cookie = element.decode(as: binder_uintptr_t.self) else { + throw BinderError.unexpectedReturn(element.command) + } + // The handler runs before the acknowledgement rather than after, so + // that a client which reacts by dropping its reference has already + // done so when the driver tears the recipient down. + // + // The registration is left in place: a cancel may be in flight, and + // BR_CLEAR_DEATH_NOTIFICATION_DONE is what actually retires it. + deaths.notification(for: UInt64(cookie))?.handler() + return .deathAcknowledged(cookie) + + case .clearDeathNotificationDone: + guard let cookie = element.decode(as: binder_uintptr_t.self) else { + throw BinderError.unexpectedReturn(element.command) + } + // The driver has confirmed the cancel, so nothing further can + // arrive for this cookie and the record can go. + _ = deaths.remove(cookie: UInt64(cookie)) + return .ignore + + case .error: + throw BinderError.driver(element.decode(as: Int32.self) ?? 0) + + case .failedReply: + throw BinderError.failedTransaction + + case .deadReply: + throw BinderError.deadObject + + default: + throw BinderError.unexpectedReturn(element.command) + } + } + + /// Route an incoming transaction to the object it names. + private func dispatch( + _ data: binder_transaction_data + ) throws(BinderError) -> IncomingAction { + let isOneWay = TransactionFlags(rawValue: data.flags).contains(.oneWay) + + // The driver returns the value we registered the node with. + let id = UInt64(data.target.ptr) + guard let object = localObjects.object(for: id) else { + // Withdrawn between the driver deciding to deliver and us looking + // it up, or a node this process never registered. Either way the + // buffer is still ours to release. + return isOneWay ? .free(data.data.ptr.buffer) : .reply(Parcel(), to: data) + } + + let request = try copyReply(data) + let transaction = LocalObject.Transaction( + code: data.code, + request: request, + flags: TransactionFlags(rawValue: data.flags), + senderProcessID: data.sender_pid, + senderUserID: data.sender_euid + ) + + // A handler that throws must not unwind into the loop: the loop cannot + // report it and would have to stop, taking every other hosted object + // down with it. The caller is told instead, through the reply. + let reply: Parcel? + do { + reply = try object.handler(transaction) + } catch { + return isOneWay ? .free(data.data.ptr.buffer) : .reply(Parcel(), to: data) + } + + // A one-way caller is not waiting, so any reply is discarded rather + // than sent — replying would be a protocol error, not merely wasteful. + return isOneWay + ? .free(data.data.ptr.buffer) + : .reply(reply ?? Parcel(), to: data) + } +} + +// MARK: - Performing + +internal extension BinderConnection { + + /// Carry out a decided action. + func perform(_ action: IncomingAction) throws(BinderError) { + switch action { + case .ignore: + return + + case .free(let pointer): + var commands = CommandStream() + commands.append(.freeBuffer, payload: pointer) + try send(commands) + + case .acknowledge(let command, let cookie): + var commands = CommandStream() + commands.append(command, payload: cookie) + try send(commands) + + case .deathAcknowledged(let cookie): + var commands = CommandStream() + commands.append(.deadBinderDone, payload: cookie) + try send(commands) + + case .reply(let parcel, let data): + try sendReply(parcel, to: data) + } + } + + /// Send `BC_REPLY` and release the request's buffer. + /// + /// The reply's bytes and offsets are staged in memory this call owns, for + /// the same reason an outgoing transaction stages its request: the driver + /// reads them through pointers rather than copying at the time the command + /// is queued. + private func sendReply( + _ parcel: Parcel, + to request: binder_transaction_data + ) throws(BinderError) { + let staged = UnsafeMutableBufferPointer + .allocate(capacity: Swift.max(parcel.bytes.count, 1)) + defer { staged.deallocate() } + _ = staged.initialize(fromContentsOf: parcel.bytes) + + let offsets = UnsafeMutableBufferPointer + .allocate(capacity: Swift.max(parcel.objectOffsets.count, 1)) + defer { offsets.deallocate() } + _ = offsets.initialize( + fromContentsOf: parcel.objectOffsets.map { binder_size_t($0) } + ) + + // Out-of-line buffers in a reply are staged and fixed up exactly as in + // an outgoing transaction; they must outlive the ioctl. + var stagedBuffers: [UnsafeMutableBufferPointer] = [] + defer { stagedBuffers.forEach { $0.deallocate() } } + for buffer in parcel.scatterGatherBuffers { + let payload = UnsafeMutableBufferPointer + .allocate(capacity: Swift.max(buffer.payload.count, 1)) + _ = payload.initialize(fromContentsOf: buffer.payload) + stagedBuffers.append(payload) + + let address = binder_uintptr_t(UInt(bitPattern: payload.baseAddress)) + withUnsafeBytes(of: address) { source in + for index in 0 ..< source.count { + staged[buffer.offset + 8 + index] = source[index] + } + } + } + + var data = binder_transaction_data() + // A reply has no target: the driver matches it to the transaction this + // thread is currently answering. + data.target.ptr = 0 + data.cookie = 0 + data.code = 0 + data.flags = 0 + data.data_size = binder_size_t(parcel.bytes.count) + data.offsets_size = binder_size_t( + parcel.objectOffsets.count * MemoryLayout.size + ) + data.data.ptr.buffer = binder_uintptr_t(UInt(bitPattern: staged.baseAddress)) + data.data.ptr.offsets = binder_uintptr_t(UInt(bitPattern: offsets.baseAddress)) + + var commands = CommandStream() + if parcel.hasBuffers { + var scatterGather = binder_transaction_data_sg() + scatterGather.transaction_data = data + scatterGather.buffers_size = binder_size_t(parcel.buffersSize) + commands.append(.replySg, payload: scatterGather) + } else { + commands.append(.reply, payload: data) + } + // The request's buffer goes back in the same stream, after the reply + // has been queued from it. + commands.append(.freeBuffer, payload: request.data.ptr.buffer) + try send(commands) + } + + /// Hand commands to the driver without waiting for anything in particular. + func send(_ commands: CommandStream) throws(BinderError) { + var pending = commands + let readBuffer = UnsafeMutableBufferPointer.allocate(capacity: 256) + defer { readBuffer.deallocate() } + readBuffer.initialize(repeating: 0) + + _ = try talkWithDriver(&pending, into: readBuffer) + } +} + +// MARK: - The loop + +public extension BinderConnection { + + /// Serve incoming transactions on the calling thread until told to stop. + /// + /// Blocks. The driver parks the thread whenever there is nothing to deliver, + /// which is what makes this cheap to leave running, and also what makes it + /// impossible for the loop to stop itself — ``stopServing()`` sets the flag, + /// but the thread stays parked until the next command arrives. A loop with + /// no traffic can therefore outlive the request to stop; closing the + /// connection is what guarantees it ends. + /// + /// More than one thread may serve the same connection. Handlers then run + /// concurrently with themselves, which is the normal arrangement for a + /// binder service and the reason ``LocalObject/Handler`` is `@Sendable`. + /// + /// - Parameter shouldContinue: consulted between commands, in addition to + /// the connection's own serving flag. + func serve(while shouldContinue: @Sendable () -> Bool = { true }) throws(BinderError) { + isServing.withLock { $0 = true } + + // Registers this thread as one the driver may deliver transactions to. + var enter = CommandStream() + enter.append(.enterLooper) + try send(enter) + + defer { + var exit = CommandStream() + exit.append(.exitLooper) + try? send(exit) + // Release the driver's per-thread state now that this thread is + // leaving binder. BC_EXIT_LOOPER only tells the driver to stop + // delivering transactions here; the binder_thread itself lives + // until BINDER_THREAD_EXIT or device close. + try? exitThread() + } + + let readBuffer = UnsafeMutableBufferPointer.allocate(capacity: 256) + defer { readBuffer.deallocate() } + readBuffer.initialize(repeating: 0) + + while isServing.withLock({ $0 }) && shouldContinue() { + var empty = CommandStream() + let returned = try talkWithDriver(&empty, into: readBuffer) + var stream = ReturnStream(bytes: returned) + + while true { + let element: ReturnStream.Element? + do { + element = try stream.next() + } catch { + throw BinderError.stream(error) + } + guard let element else { break } + + try perform(try action(for: element)) + } + } + } + + /// Ask every serving loop on this connection to stop. + /// + /// Takes effect when each loop next returns from the driver, which may be + /// not at all if nothing is being delivered — see ``serve(while:)``. + func stopServing() { + isServing.withLock { $0 = false } + } + + /// Whether a serving loop has been started and not yet asked to stop. + var isServingTransactions: Bool { + isServing.withLock { $0 } + } +} diff --git a/Sources/Binder/MappedBuffer.swift b/Sources/Binder/MappedBuffer.swift new file mode 100644 index 0000000..4cbf2ad --- /dev/null +++ b/Sources/Binder/MappedBuffer.swift @@ -0,0 +1,109 @@ +// +// MappedBuffer.swift +// Binder +// + +import SystemPackage + +// mmap/munmap/sysconf come from the platform C library, which is a different +// module on each. Android is the point of this library, so its Bionic module +// has to be here — without it `canImport(Glibc)` is false on Android and none +// of these symbols resolve. The module is spelled `Android` on the swift.org +// Android SDK and `Bionic` on some toolchains, so both are covered. +#if canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(Android) +import Android +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Darwin) +import Darwin +#endif + +/// The region of memory the driver delivers incoming transactions into. +/// +/// Binder does not copy transaction payloads into a buffer the caller supplies. +/// It writes them into a mapping the caller established up front, and hands +/// back pointers into that mapping. So the mapping is not an optimisation that +/// can be added later: until it exists the driver has nowhere to put a reply, +/// and every transaction fails. +/// +/// The mapping is read-only by design. Userspace never writes here — outgoing +/// data travels in the write buffer of a `BINDER_WRITE_READ` ioctl, and the +/// kernel is the only writer of this region. Asking for `PROT_WRITE` fails. +/// +/// Ownership: the mapping lives exactly as long as the ``Binder`` that made it, +/// which is why this is a non-copyable type that unmaps on `deinit` rather than +/// something a caller could duplicate. +internal struct MappedBuffer: ~Copyable { + + /// The start of the mapping. + internal let address: UnsafeMutableRawPointer + + /// The length of the mapping, needed again to unmap it. + internal let size: Int + + private init(address: UnsafeMutableRawPointer, size: Int) { + self.address = address + self.size = size + } + + deinit { + // Nothing useful can be done if this fails, and throwing is not + // possible from deinit. A failure here means the address or length no + // longer describes a mapping, which is a bug in this type rather than + // a runtime condition. + assert(munmap(address, size) == 0, "Unable to unmap binder buffer") + } +} + +internal extension MappedBuffer { + + /// The size libbinder maps, and therefore the size the driver and every + /// service on the other end expect. + /// + /// One megabyte less two pages. The subtraction is not decoration: the + /// kernel accounts its own overhead inside the mapping, and a process that + /// asks for a round megabyte gets less usable space than services assume. + static var defaultSize: Int { + (1 * 1024 * 1024) - (Int(sysconf(Int32(_SC_PAGESIZE))) * 2) + } + + /// Map a binder device's transaction buffer. + /// + /// - Parameters: + /// - fileDescriptor: an open binder device. + /// - size: bytes to map, defaulting to ``defaultSize``. + static func map( + _ fileDescriptor: FileDescriptor, + size: Int = MappedBuffer.defaultSize + ) throws(Errno) -> MappedBuffer { + // MAP_NORESERVE because the mapping is large and almost never fully + // used; reserving swap for all of it would charge every binder client + // a megabyte of commit it will not touch. + // Typed as optional so both mmap return shapes fit: Glibc and Darwin + // import it as an implicitly-unwrapped optional, Bionic as a plain + // pointer, and a non-optional coerces into the optional binding while + // an IUO stays bindable. + let result: UnsafeMutableRawPointer? = mmap( + nil, + size, + PROT_READ, + MAP_PRIVATE | MAP_NORESERVE, + fileDescriptor.rawValue, + 0 + ) + + // MAP_FAILED is ((void *) -1). Comparing the bit pattern rather than the + // macro keeps this working on Bionic, whose Swift overlay does not + // import MAP_FAILED. + guard let address = result, + address != UnsafeMutableRawPointer(bitPattern: -1) else { + throw Errno(rawValue: errno) + } + + return MappedBuffer(address: address, size: size) + } +} diff --git a/Sources/Binder/MaxThreads.swift b/Sources/Binder/MaxThreads.swift new file mode 100644 index 0000000..7e114d7 --- /dev/null +++ b/Sources/Binder/MaxThreads.swift @@ -0,0 +1,71 @@ +// +// MaxThreads.swift +// Binder +// + +import SystemPackage +import Socket +import CBinder + +internal extension Binder { + + /// The `BINDER_SET_MAX_THREADS` payload. + /// + /// A plain `__u32` on the wire. It needs a type of its own only because the + /// ioctl wrapper dispatches on `IOControlValue.id`, which is how the + /// request number gets attached to the value being sent. + struct MaxThreads { + + var value: UInt32 + + init(_ value: UInt32) { + self.value = value + } + } +} + +extension Binder.MaxThreads: IOControlValue { + + static var id: BinderCommand { .setMaxThreads } + + mutating func withUnsafeMutablePointer( + _ body: (UnsafeMutableRawPointer) throws -> (Result) + ) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &self) { buffer in + try body(buffer.baseAddress!) + } + } +} + +public extension Binder { + + /// The thread-pool size libbinder requests, and therefore what services on + /// the other end are provisioned to expect. + /// + /// This caps how many threads the *driver* will ask this process to spawn + /// to service incoming transactions. It does not start any threads. + static var defaultMaxThreads: UInt32 { 15 } + + /// Tell the driver how many threads it may ask this process to spawn. + /// + /// Raising it past what the process can actually service does not help: + /// the driver will hand out work to threads that do not exist and callers + /// will block. Lowering it below what a busy service needs serialises + /// incoming transactions. + func setMaxThreads(_ count: UInt32) throws(Errno) { + try handle.setMaxThreads(count) + } +} + +internal extension Binder.Handle { + + func setMaxThreads(_ count: UInt32) throws(Errno) { + var value = Binder.MaxThreads(count) + #if ENABLE_MOCKING + assert(type(of: value).id == .setMaxThreads) + assert(type(of: value).id.rawValue == BINDER_SET_MAX_THREADS) + #else + try fileDescriptor.inputOutput(&value) + #endif + } +} diff --git a/Sources/Binder/Parcel.swift b/Sources/Binder/Parcel.swift new file mode 100644 index 0000000..4293681 --- /dev/null +++ b/Sources/Binder/Parcel.swift @@ -0,0 +1,400 @@ +// +// Parcel.swift +// Binder +// + +/// A binder parcel: the serialised payload of a transaction. +/// +/// The layout is Android's, because the process on the other end is Android's. +/// Three rules govern all of it: +/// +/// - Every write is padded to a 4-byte boundary, and the padding is zeroed. +/// `Parcel.cpp` does this by masking the trailing word; appending zero bytes +/// is the same thing. +/// - Values are written in the **host's** byte order, not a fixed one. A parcel +/// is shared memory between two processes on one machine, never a network +/// format, so `memcpy` semantics are what the other side expects. +/// - `Bool`, `Int8` and `UTF16.CodeUnit` are all widened to `Int32` on the +/// wire. There is no one-byte encoding of a scalar in a parcel. +/// +/// `Int64` is the trap in that scheme: it occupies 8 bytes but is only ever +/// 4-byte aligned, because nothing pads to 8. Reading it as an aligned 64-bit +/// load is undefined behaviour on the receiving side, and it is why this type +/// copies bytes rather than binding memory. +public struct Parcel: Equatable, Hashable, Sendable { + + /// The serialised bytes, including any trailing padding. + public private(set) var bytes: [UInt8] + + /// Where each `flat_binder_object` starts within ``bytes``. + /// + /// The driver cannot find binder objects by scanning — a payload is opaque + /// bytes to it, and a value that happens to look like an object header + /// would be indistinguishable from one. So a transaction carries an + /// explicit array of offsets, and the driver rewrites a handle only at the + /// positions named there. Anything not listed crosses as plain data, which + /// means an object written without recording its offset arrives as a + /// number that addresses something else in the receiving process. + public private(set) var objectOffsets: [Int] + + /// Out-of-line buffer payloads and where their descriptors sit. + /// + /// Each entry is the offset of a `binder_buffer_object` within ``bytes`` and + /// the bytes it names, which live here rather than inline. The transaction + /// path stages the payload and writes its address into the descriptor at + /// send time; see ``append(buffer:)``. + internal private(set) var scatterGatherBuffers: [(offset: Int, payload: [UInt8])] + + /// An empty parcel. + public init() { + self.bytes = [] + self.objectOffsets = [] + self.scatterGatherBuffers = [] + } + + /// A parcel wrapping bytes already received from the driver. + public init(bytes: [UInt8]) { + self.bytes = bytes + self.objectOffsets = [] + self.scatterGatherBuffers = [] + } + + internal init(bytes: [UInt8], objectOffsets: [Int]) { + self.bytes = bytes + self.objectOffsets = objectOffsets + self.scatterGatherBuffers = [] + } + + internal init( + bytes: [UInt8], + objectOffsets: [Int], + scatterGatherBuffers: [(offset: Int, payload: [UInt8])] + ) { + self.bytes = bytes + self.objectOffsets = objectOffsets + self.scatterGatherBuffers = scatterGatherBuffers + } +} + +// MARK: - Equatable, Hashable + +// Synthesised conformances would ignore the scatter-gather payloads, since the +// tuple array is not itself Equatable; these fold them in so two parcels that +// differ only in out-of-line data are not treated as equal. +public extension Parcel { + + static func == (lhs: Parcel, rhs: Parcel) -> Bool { + lhs.bytes == rhs.bytes + && lhs.objectOffsets == rhs.objectOffsets + && lhs.scatterGatherBuffers.count == rhs.scatterGatherBuffers.count + && zip(lhs.scatterGatherBuffers, rhs.scatterGatherBuffers).allSatisfy { + $0.offset == $1.offset && $0.payload == $1.payload + } + } + + func hash(into hasher: inout Hasher) { + hasher.combine(bytes) + hasher.combine(objectOffsets) + for buffer in scatterGatherBuffers { + hasher.combine(buffer.offset) + hasher.combine(buffer.payload) + } + } +} + +// MARK: - Layout + +public extension Parcel { + + /// The number of bytes written so far. + var count: Int { + bytes.count + } + + var isEmpty: Bool { + bytes.isEmpty + } + + /// Round a length up to the next 4-byte boundary. + /// + /// `PAD_SIZE_UNSAFE` in `Parcel.cpp`. + static func paddedSize(_ length: Int) -> Int { + (length + 3) & ~3 + } +} + +// MARK: - Writing + +public extension Parcel { + + /// Append a value's raw bytes, padded out to a 4-byte boundary. + /// + /// Host byte order, matching the `memcpy` in `Parcel::writeAligned`. + mutating func append(bitPattern value: T) { + withUnsafeBytes(of: value) { bytes.append(contentsOf: $0) } + pad() + } + + mutating func append(_ value: Int32) { + append(bitPattern: value) + } + + /// Append a 64-bit integer. + /// + /// Occupies 8 bytes at 4-byte alignment. Nothing aligns a parcel to 8, so a + /// reader must not assume it can load this as an aligned `Int64`. + mutating func append(_ value: Int64) { + append(bitPattern: value) + } + + /// Append a boolean, widened to `Int32` as `Parcel::writeBool` does. + mutating func append(_ value: Bool) { + append(Int32(value ? 1 : 0)) + } + + /// Append a signed byte, widened to `Int32` as `Parcel::writeByte` does. + /// + /// Note this costs four bytes on the wire, not one. For a run of bytes use + /// ``append(bytes:)``, which does not widen. + mutating func append(byte value: Int8) { + append(Int32(value)) + } + + /// Append a UTF-16 code unit, widened to `Int32` as `Parcel::writeChar`. + mutating func append(character value: UTF16.CodeUnit) { + append(Int32(value)) + } + + /// Append a string in Android's `String16` encoding. + /// + /// A length in **UTF-16 code units** — not bytes, and not characters — + /// followed by that many code units plus a NUL terminator, padded to 4. + /// The terminator is included in the bytes but not in the length, and the + /// receiver validates that it is there. + mutating func append(_ value: String) { + let units = Array(value.utf16) + append(Int32(units.count)) + for unit in units { + withUnsafeBytes(of: unit) { bytes.append(contentsOf: $0) } + } + withUnsafeBytes(of: UTF16.CodeUnit(0)) { bytes.append(contentsOf: $0) } + pad() + } + + /// Append a null string, which is a length of `-1` and nothing else. + /// + /// Distinct from the empty string, which writes a length of `0` and a + /// terminator. A reader can tell them apart and the difference is + /// meaningful to AIDL, so it is not collapsed here. + mutating func appendNullString() { + append(Int32(-1)) + } + + /// Append a length-prefixed run of bytes, not widened. + mutating func append(bytes value: [UInt8]) { + append(Int32(value.count)) + bytes.append(contentsOf: value) + pad() + } + + /// Append a null byte array, which is a length of `-1`. + mutating func appendNullBytes() { + append(Int32(-1)) + } + + /// Note that a binder object starts at the current end of the payload. + /// + /// Kept here rather than exposing the setter, so the offsets cannot drift + /// away from the bytes they describe. + internal mutating func recordObjectOffset() { + objectOffsets.append(bytes.count) + } + + /// Note an out-of-line buffer: where its descriptor sits and the payload it + /// names. The descriptor's offset is also recorded as an object offset by + /// the caller, since the driver processes it alongside the others. + internal mutating func recordBuffer(at offset: Int, payload: [UInt8]) { + scatterGatherBuffers.append((offset: offset, payload: payload)) + } + + /// Zero-fill up to the next 4-byte boundary. + private mutating func pad() { + let padded = Parcel.paddedSize(bytes.count) + bytes.append(contentsOf: repeatElement(0, count: padded - bytes.count)) + } +} + +// MARK: - Errors + +/// A parcel could not be decoded. +public enum ParcelError: Error, Equatable, Hashable, Sendable { + + /// The parcel ended before the requested value did. + /// + /// Reached whenever a reader is pointed at a reply that is shorter than the + /// interface says it should be — a version mismatch between the two sides, + /// most often. + case endOfParcel + + /// A length prefix was negative where null is not permitted, or large + /// enough that reading it would run past the end. + case invalidLength(Int32) + + /// A `String16` was not NUL-terminated. + /// + /// Android treats this as a security event rather than a plain decode + /// failure, because it means the length prefix and the payload disagree. + case unterminatedString + + /// The four-character RPC marker did not match the expected one. + /// + /// The two sides were built against different binder configurations — + /// system versus vendor versus off-Android. The callee rejects the whole + /// transaction on this, so it is worth reporting distinctly rather than as + /// a generic decode failure. + case interfaceMismatch(expected: Int32, found: Int32) + + /// The caller named a different interface than the one being served. + /// + /// Distinct from ``interfaceMismatch(expected:found:)``, which is about the + /// build marker: this is the descriptor string itself disagreeing, meaning + /// the transaction was routed to an object that does not implement what the + /// caller asked for. + case wrongInterface(expected: String, found: String?) +} + +// MARK: - Reading + +public extension Parcel { + + /// A cursor over a parcel's contents. + /// + /// A separate type rather than a mutating position on `Parcel` so that a + /// received parcel stays immutable while it is being decoded, and so two + /// readers over one parcel cannot interfere. + struct Reader: Equatable, Hashable, Sendable { + + internal let bytes: [UInt8] + + /// The offset of the next byte to be read. + public private(set) var offset: Int + + internal init(bytes: [UInt8]) { + self.bytes = bytes + self.offset = 0 + } + + /// Whether every byte has been consumed. + /// + /// Padding counts as consumed, so a fully-decoded parcel reports true. + public var isAtEnd: Bool { + offset >= bytes.count + } + + /// The number of bytes not yet read. + public var remaining: Int { + bytes.count - offset + } + } + + /// A cursor positioned at the start of this parcel. + func reader() -> Reader { + Reader(bytes: bytes) + } +} + +public extension Parcel.Reader { + + /// Consume `length` bytes, plus whatever padding follows them. + /// + /// The padding is stepped over here rather than by each caller, because + /// forgetting it desynchronises the cursor and every subsequent read + /// returns plausible garbage rather than failing. + mutating func read(count length: Int) throws(ParcelError) -> ArraySlice { + let padded = Parcel.paddedSize(length) + guard length >= 0, offset + padded <= bytes.count else { + throw ParcelError.endOfParcel + } + let value = bytes[offset ..< offset + length] + offset += padded + return value + } + + /// Read a value's raw bytes in host order. + mutating func read(bitPattern type: T.Type) throws(ParcelError) -> T { + let slice = try read(count: MemoryLayout.size) + return Array(slice).withUnsafeBytes { $0.loadUnaligned(as: T.self) } + } + + mutating func readInt32() throws(ParcelError) -> Int32 { + try read(bitPattern: Int32.self) + } + + /// Read a 64-bit integer. + /// + /// Loaded unaligned on purpose: the value sits at 4-byte alignment and an + /// aligned load would be undefined behaviour. + mutating func readInt64() throws(ParcelError) -> Int64 { + try read(bitPattern: Int64.self) + } + + /// Read a boolean. + /// + /// Any non-zero value is true, matching how the Java and native readers + /// treat it. A sender is not obliged to normalise to 1. + mutating func readBool() throws(ParcelError) -> Bool { + try readInt32() != 0 + } + + /// Read a signed byte from its widened `Int32` encoding. + mutating func readByte() throws(ParcelError) -> Int8 { + Int8(truncatingIfNeeded: try readInt32()) + } + + /// Read a UTF-16 code unit from its widened `Int32` encoding. + mutating func readCharacter() throws(ParcelError) -> UTF16.CodeUnit { + UTF16.CodeUnit(truncatingIfNeeded: try readInt32()) + } + + /// Read a string in Android's `String16` encoding. + /// + /// Returns `nil` for a null string, which is encoded as a length of `-1` + /// and is distinct from the empty string. + mutating func readString() throws(ParcelError) -> String? { + let length = try readInt32() + guard length != -1 else { return nil } + guard length >= 0, length < Int32.max else { + throw ParcelError.invalidLength(length) + } + + // The terminator is on the wire but not in the length. + let count = Int(length) + 1 + let slice = try read(count: count * MemoryLayout.size) + + var units = [UTF16.CodeUnit]() + units.reserveCapacity(count) + Array(slice).withUnsafeBytes { buffer in + for index in 0 ..< count { + units.append(buffer.loadUnaligned( + fromByteOffset: index * MemoryLayout.size, + as: UTF16.CodeUnit.self + )) + } + } + + guard units.last == 0 else { + throw ParcelError.unterminatedString + } + return String(decoding: units.dropLast(), as: UTF16.self) + } + + /// Read a length-prefixed run of bytes. + /// + /// Returns `nil` for a null array, encoded as a length of `-1`. + mutating func readBytes() throws(ParcelError) -> [UInt8]? { + let length = try readInt32() + guard length != -1 else { return nil } + guard length >= 0 else { throw ParcelError.invalidLength(length) } + return Array(try read(count: Int(length))) + } +} diff --git a/Sources/Binder/ReferenceCounting.swift b/Sources/Binder/ReferenceCounting.swift new file mode 100644 index 0000000..4efaec9 --- /dev/null +++ b/Sources/Binder/ReferenceCounting.swift @@ -0,0 +1,79 @@ +// +// ReferenceCounting.swift +// Binder +// + +import CBinder + +/// Reference counts on remote handles. +/// +/// A handle is not a pointer the holder can keep indefinitely. The driver +/// maintains a per-process reference count for every handle it has issued, and +/// reclaims the handle — making it available for reuse by a *different* object — +/// once the count reaches zero. A handle held without a reference is therefore +/// worse than dangling: it may silently start addressing something else. +/// +/// The counts are two, not one. A strong reference keeps the target object +/// alive; a weak reference only keeps the handle from being recycled, and can +/// be promoted to strong if the target is still there. This mirrors `sp<>` and +/// `wp<>` in libbinder. +/// +/// ## Who owns what +/// +/// When the driver delivers a transaction containing a binder object, it takes +/// a strong reference on the receiver's behalf and ties it to the *buffer* — +/// `binder_transaction_buffer_release` drops it when the buffer is handed back +/// with `BC_FREE_BUFFER`. So a handle read out of a reply is only valid until +/// that free, and anything wanting to keep it must take its own reference +/// first. ``BinderConnection`` does that automatically for every object in a +/// reply, before releasing the buffer; the resulting reference is what +/// ``RemoteProxy`` adopts and eventually releases. +public extension BinderConnection { + + /// Take a strong reference to a handle. + /// + /// Keeps the target object alive, not merely the handle. + func acquire(_ object: RemoteObject) throws(BinderError) { + try send(.acquire, handle: object.handle) + } + + /// Drop a strong reference previously taken with ``acquire(_:)``. + /// + /// Releasing more times than acquired corrupts the driver's accounting for + /// this process and will eventually invalidate an unrelated handle, so the + /// pairing is worth keeping mechanical — see ``RemoteProxy``. + func release(_ object: RemoteObject) throws(BinderError) { + try send(.release, handle: object.handle) + } + + /// Take a weak reference to a handle. + /// + /// Stops the handle being recycled but does not keep the target alive. + func incrementWeak(_ object: RemoteObject) throws(BinderError) { + try send(.incRefs, handle: object.handle) + } + + /// Drop a weak reference previously taken with ``incrementWeak(_:)``. + func decrementWeak(_ object: RemoteObject) throws(BinderError) { + try send(.decRefs, handle: object.handle) + } + + /// Send one reference-counting command. + /// + /// These carry a bare `__u32` handle and produce no reply, so there is + /// nothing to wait for — the driver applies them as it consumes the write + /// buffer. + private func send( + _ command: DriverCommandProtocol, + handle: UInt32 + ) throws(BinderError) { + var commands = CommandStream() + commands.append(command, payload: handle) + + let readBuffer = UnsafeMutableBufferPointer.allocate(capacity: 256) + defer { readBuffer.deallocate() } + readBuffer.initialize(repeating: 0) + + _ = try talkWithDriver(&commands, into: readBuffer) + } +} diff --git a/Sources/Binder/RemoteObject.swift b/Sources/Binder/RemoteObject.swift new file mode 100644 index 0000000..c1b864c --- /dev/null +++ b/Sources/Binder/RemoteObject.swift @@ -0,0 +1,121 @@ +// +// RemoteObject.swift +// Binder +// + +import CBinder + +/// A reference to an object living in another process. +/// +/// Just a handle. The number is meaningful only to the driver and only for the +/// process that received it: the same remote object has different handles in +/// different processes, and a handle passed between processes as an integer +/// refers to nothing. The driver translates handles when a +/// ``flat_binder_object`` crosses a transaction boundary, which is why sending +/// one has to go through ``Parcel/append(_:)-(RemoteObject)`` rather than +/// writing the number. +/// +/// This type is the plain value — it does **not** own a reference. A handle is +/// only valid while this process holds a reference to it, and the driver may +/// reissue the number for a different object once the count reaches zero, so a +/// bare `RemoteObject` kept across time can silently start addressing something +/// else. ``RemoteProxy`` is the owning form; this one is for parcelling, +/// comparison, and passing to ``BinderConnection/adopt(_:)``. +public struct RemoteObject: Equatable, Hashable, Sendable { + + /// The driver's handle for this object in this process. + public let handle: UInt32 + + public init(handle: UInt32) { + self.handle = handle + } +} + +public extension RemoteObject { + + /// The context manager, which is always handle zero. + /// + /// Fixed by the protocol rather than discovered: a process has no way to + /// look up the service manager, because looking anything up requires + /// talking to it first. + static var contextManager: RemoteObject { + RemoteObject(handle: 0) + } +} + +// MARK: - Parcelling + +public extension Parcel { + + /// Append a reference to a remote object. + /// + /// Written as a `flat_binder_object` inline in the payload, and its + /// position recorded in ``objectOffsets``. The driver rewrites the handle + /// for the receiving process, but only at positions the transaction names — + /// it cannot find objects by scanning, since a payload is opaque bytes to + /// it and an integer that happens to look like an object header would be + /// indistinguishable from one. An object written without its offset + /// recorded arrives as a number addressing something else entirely. + mutating func append(_ value: RemoteObject) { + var object = flat_binder_object() + object.hdr = binder_object_header(type: .handle) + object.flags = FlatBinderObjectFlags.acceptsFDs.rawValue + object.handle = value.handle + object.cookie = 0 + + recordObjectOffset() + append(bitPattern: object) + } +} + +public extension Parcel { + + /// Every binder object this parcel carries, in wire order. + /// + /// For a reply, these are the objects the driver translated into this + /// process, each of which arrived holding a strong reference. + var objects: [RemoteObject] { + objectOffsets.compactMap { offset in + let size = MemoryLayout.size + guard offset >= 0, offset + size <= bytes.count else { return nil } + let object = Array(bytes[offset ..< offset + size]) + .withUnsafeBytes { $0.loadUnaligned(as: flat_binder_object.self) } + guard let type = BinderType(rawValue: object.hdr.type) else { return nil } + switch type { + case .handle, .weakHandle: + return RemoteObject(handle: object.handle) + default: + return nil + } + } + } +} + +public extension Parcel.Reader { + + /// Read a reference to a remote object. + /// + /// - Returns: the object, or `nil` if the sender wrote a null binder — a + /// `flat_binder_object` whose handle is zero and whose type is a binder + /// rather than a handle. A null binder is how a service manager says "no + /// such service", so it is a normal answer rather than an error. + mutating func readObject() throws(ParcelError) -> RemoteObject? { + let slice = try read(count: MemoryLayout.size) + let object = Array(slice).withUnsafeBytes { + $0.loadUnaligned(as: flat_binder_object.self) + } + + guard let type = BinderType(rawValue: object.hdr.type) else { + return nil + } + switch type { + case .handle, .weakHandle: + return RemoteObject(handle: object.handle) + default: + // A local binder pointer, a file descriptor or a buffer. None of + // those is a remote object reference, and guessing a handle out of + // the union would produce a number that addresses something else. + return nil + } + } +} diff --git a/Sources/Binder/RemoteProxy.swift b/Sources/Binder/RemoteProxy.swift new file mode 100644 index 0000000..3dfc9b4 --- /dev/null +++ b/Sources/Binder/RemoteProxy.swift @@ -0,0 +1,125 @@ +// +// RemoteProxy.swift +// Binder +// + +/// An owned strong reference to a remote object. +/// +/// A ``RemoteObject`` is just a handle — a number the driver assigned, valid +/// only in this process and only while this process holds a reference to it. +/// Nothing about the value says who is responsible for that reference. This +/// type is what makes it accountable: it holds one strong reference for its +/// lifetime and releases it on `deinit`, so the driver's count and Swift's +/// object graph agree without anyone having to remember. +/// +/// Getting that wrong is not a leak in the ordinary sense. Releasing once too +/// often lets the driver reclaim the handle and hand the same number to a +/// *different* object, so a stale proxy silently starts calling something else. +/// Releasing once too rarely keeps the target process alive indefinitely. +/// Neither shows up where it was caused, which is why the pairing is mechanical +/// here rather than left to call sites. +/// +/// ## Adoption, not acquisition +/// +/// The initialiser **adopts** an existing reference rather than taking a new +/// one. Objects arrive from the driver already holding a strong reference — +/// ``BinderConnection`` takes one for every object in a reply before releasing +/// the transaction buffer — so acquiring again here would double the count and +/// keep the target alive forever. Use ``BinderConnection/adopt(_:)`` on an +/// object that came out of a reply, and ``BinderConnection/retain(_:)`` on one +/// obtained some other way. +public final class RemoteProxy { + + /// The underlying handle, for parcelling or comparison. + /// + /// Passing this somewhere that outlives the proxy reintroduces exactly the + /// problem the proxy exists to solve. + public let object: RemoteObject + + internal let connection: BinderConnection + + /// Adopt a strong reference that has already been taken. + internal init(adopting object: RemoteObject, connection: BinderConnection) { + self.object = object + self.connection = connection + } + + deinit { + // Nothing useful can be done with a failure here and `deinit` cannot + // throw. A failed release means the driver already dropped this + // process's reference, which is a bug in this type's accounting rather + // than a runtime condition. + try? connection.release(object) + } +} + +extension RemoteProxy: @unchecked Sendable {} + +public extension RemoteProxy { + + /// The handle the driver assigned. + var handle: UInt32 { + object.handle + } + + /// Send a transaction to this object and wait for its reply. + /// + /// See ``BinderConnection/transact(with:code:request:flags:)`` for what the + /// reply's own objects require. + func transact( + code: UInt32, + request: Parcel, + flags: TransactionFlags = .acceptFds + ) throws(BinderError) -> Parcel { + try connection.transact( + with: object, + code: code, + request: request, + flags: flags + ) + } + + /// Send a transaction and suspend until the reply arrives. + func transact( + code: UInt32, + request: Parcel, + flags: TransactionFlags = .acceptFds + ) async throws(BinderError) -> Parcel { + try await connection.transact( + with: object, + code: code, + request: request, + flags: flags + ) + } + + /// A weak reference to the same object. + /// + /// Holds its own weak count, independent of this strong one, so it outlives + /// this proxy and can be promoted again later — as long as the object is + /// still alive when it is. + func weak() throws(BinderError) -> WeakProxy { + try connection.weakReference(to: object) + } +} + +public extension BinderConnection { + + /// Take ownership of a reference this connection already holds. + /// + /// For objects read out of a reply, whose reference was taken when the + /// reply's buffer was released. + func adopt(_ object: RemoteObject) -> RemoteProxy { + RemoteProxy(adopting: object, connection: self) + } + + /// Take a new strong reference and own it. + /// + /// For a handle obtained some way other than from a reply — reconstructed + /// from a stored number, say. Acquiring a handle this process does not + /// already hold a reference to is rejected by the driver. + func retain(_ object: RemoteObject) throws(BinderError) -> RemoteProxy { + try acquire(object) + return RemoteProxy(adopting: object, connection: self) + } +} diff --git a/Sources/Binder/ReturnStream.swift b/Sources/Binder/ReturnStream.swift new file mode 100644 index 0000000..678b6ab --- /dev/null +++ b/Sources/Binder/ReturnStream.swift @@ -0,0 +1,117 @@ +// +// ReturnStream.swift +// Binder +// + +import CBinder + +/// A cursor over the `BR_*` commands the driver wrote into the read buffer. +/// +/// The driver answers a `BINDER_WRITE_READ` with a stream, not a result. A +/// single reply is routinely preceded by `BR_NOOP` and +/// `BR_TRANSACTION_COMPLETE`, and may be interleaved with reference-count +/// commands and `BR_SPAWN_LOOPER` that have nothing to do with the transaction +/// being waited on. So the caller cannot read "the answer" — it has to walk the +/// stream and decide which command ends its wait. +/// +/// Unknown commands are a decode failure rather than something to step over: a +/// command this build does not recognise has an unknown payload length, so +/// there is no way to find where the next one starts. Guessing would turn one +/// unknown command into a stream of plausible nonsense. +public struct ReturnStream: Equatable, Hashable, Sendable { + + internal let bytes: [UInt8] + + /// The offset of the next command. + public private(set) var offset: Int + + public init(bytes: [UInt8]) { + self.bytes = bytes + self.offset = 0 + } + + public var isAtEnd: Bool { + offset >= bytes.count + } +} + +/// A `BR_*` command could not be decoded. +public enum ReturnStreamError: Error, Equatable, Hashable, Sendable { + + /// The stream ended part-way through a command or its payload. + case truncated + + /// A command code this build does not know. + /// + /// Its payload length is unknown, so the rest of the stream cannot be + /// walked. Usually means the driver is newer than these headers. + case unknownCommand(UInt32) +} + +public extension ReturnStream { + + /// One command and its payload bytes. + struct Element: Equatable, Hashable, Sendable { + + public let command: DriverReturnProtocol + + /// The payload, empty for commands that carry none. + public let payload: [UInt8] + + /// Decode the payload as the structure the command declares. + /// + /// The size is checked against the command code rather than trusted, + /// and the load is unaligned because the payload sits at whatever + /// offset the previous command left behind. + public func decode(as type: T.Type) -> T? { + guard payload.count == MemoryLayout.size else { return nil } + return payload.withUnsafeBytes { $0.loadUnaligned(as: T.self) } + } + } + + /// Advance to the next command, or `nil` at the end of the stream. + mutating func next() throws(ReturnStreamError) -> Element? { + guard !isAtEnd else { return nil } + + let codeSize = MemoryLayout.size + guard offset + codeSize <= bytes.count else { + throw .truncated + } + + let raw = bytes[offset ..< offset + codeSize] + .withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } + let command = DriverReturnProtocol(rawValue: raw) + + guard Self.isKnown(command) else { + throw .unknownCommand(raw) + } + + let size = command.payloadSize + guard offset + codeSize + size <= bytes.count else { + throw .truncated + } + + let payload = Array(bytes[offset + codeSize ..< offset + codeSize + size]) + offset += codeSize + size + return Element(command: command, payload: payload) + } + + /// Whether this build recognises a return command. + /// + /// Enumerated rather than range-checked because the codes are `_IOC` + /// values, not consecutive integers, and an unrecognised one cannot be + /// skipped safely. + private static func isKnown(_ command: DriverReturnProtocol) -> Bool { + switch command { + case .error, .ok, .transactionSecCtx, .transaction, .reply, + .acquireResult, .deadReply, .transactionComplete, .incRefs, + .acquire, .release, .decRefs, .attemptAcquire, .noop, + .spawnLooper, .finished, .deadBinder, + .clearDeathNotificationDone, .failedReply, .frozenReply, + .onewaySpamSuspect, .transactionPendingFrozen: + return true + default: + return false + } + } +} diff --git a/Sources/Binder/ServiceManager.swift b/Sources/Binder/ServiceManager.swift new file mode 100644 index 0000000..c17694c --- /dev/null +++ b/Sources/Binder/ServiceManager.swift @@ -0,0 +1,254 @@ +// +// ServiceManager.swift +// Binder +// + +/// The context manager: the one object whose handle every process knows. +/// +/// Bootstrapping binder requires exactly one fixed point, because looking up a +/// service means transacting with something, and the only handle a fresh +/// process has is zero. Everything else in the system is reached by asking this +/// object for it. +public enum ServiceManager { + + /// The interface descriptor every request to it must carry. + public static var descriptor: String { "android.os.IServiceManager" } + + /// Transaction codes, offset from `FIRST_CALL_TRANSACTION`. + /// + /// Positional: they are the declaration order of the methods in + /// `IServiceManager.aidl`, not values anyone chose. Inserting a method + /// upstream would renumber everything after it, which is why AIDL forbids + /// reordering rather than treating these as stable identifiers. + public enum Transaction: UInt32, Equatable, Hashable, Sendable, CaseIterable { + + /// Blocks for up to five seconds waiting for the service to appear. + case getService = 1 + + /// Returns immediately, null if the service is not registered. + case checkService = 2 + + case addService = 3 + + case listServices = 4 + } + + /// Which services `listServices` should report. + /// + /// A bitmask of dump priorities. Services register with one, and the + /// listing is filtered by it — asking for the wrong priority returns a + /// short list rather than an error, which reads as "the service is + /// missing" and sends people looking in the wrong place. + public struct DumpPriority: OptionSet, Equatable, Hashable, Sendable { + + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static var critical: DumpPriority { DumpPriority(rawValue: 1 << 0) } + public static var high: DumpPriority { DumpPriority(rawValue: 1 << 1) } + public static var normal: DumpPriority { DumpPriority(rawValue: 1 << 2) } + public static var `default`: DumpPriority { DumpPriority(rawValue: 1 << 3) } + + /// Every priority. What `listServices` uses when not told otherwise. + public static var all: DumpPriority { [.critical, .high, .normal, .default] } + } +} + +public extension BinderConnection { + + /// Look up a service, waiting for it to appear. + /// + /// Blocks in the *server* for up to five seconds if the service is not yet + /// registered — a deliberate part of the protocol, not a timeout of ours. + /// Prefer ``checkService(named:header:)`` when the caller can handle + /// absence itself, since a stall here is indistinguishable from a hung + /// service manager. + /// + /// - Returns: the service, or `nil` if it never appeared. + func service( + named name: String, + header: RPCHeader = .system + ) throws(BinderError) -> RemoteProxy? { + try lookUp(name, using: .getService, header: header) + } + + /// Look up a service without waiting. + /// + /// - Returns: the service, or `nil` if it is not currently registered. + func checkService( + named name: String, + header: RPCHeader = .system + ) throws(BinderError) -> RemoteProxy? { + try lookUp(name, using: .checkService, header: header) + } + + private func lookUp( + _ name: String, + using transaction: ServiceManager.Transaction, + header: RPCHeader + ) throws(BinderError) -> RemoteProxy? { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor, header: header) + request.append(name) + + let reply = try transact( + with: .contextManager, + code: transaction.rawValue, + request: request + ) + + var reader = reply.reader() + + // Every AIDL reply opens with an exception code. A non-zero value means + // the callee threw, and the rest of the parcel describes that rather + // than holding the declared return value — so it is checked before + // anything else is decoded. + let exception: Int32 + do { + exception = try reader.readInt32() + } catch { + throw BinderError.parcel(error) + } + guard exception == 0 else { + throw BinderError.remoteException(exception) + } + + let object: RemoteObject? + do { + object = try reader.readObject() + } catch { + throw BinderError.parcel(error) + } + + // The reply's objects already hold a strong reference, taken before the + // transaction buffer went back. Adopting it rather than acquiring again + // is what keeps the driver's count at one. + return object.map { adopt($0) } + } + + /// Register a forwarded reference under a name. + /// + /// This variant registers an object *someone else* hosts — re-registering + /// a service this process looked up, for instance. To publish an object + /// this process serves, use the ``LocalObject`` overload. + func addService( + named name: String, + object: RemoteObject, + allowIsolated: Bool = false, + dumpPriority: ServiceManager.DumpPriority = .default, + header: RPCHeader = .system + ) throws(BinderError) { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor, header: header) + request.append(name) + // Records the object's offset, which is what lets the driver rewrite + // the handle for the service manager's process. + request.append(object) + request.append(allowIsolated) + request.append(dumpPriority.rawValue) + + try finishAddService(request) + } + + /// Publish an object this process hosts under a name. + /// + /// After this returns, any process can obtain the object by name and call + /// it — the registration itself is what first carries the node to the + /// driver, so nothing has to have been sent earlier. Calls are only + /// *answered* while a serving loop runs; register first and then serve, + /// since a caller that finds the service before the loop starts merely + /// blocks until it does rather than failing. + /// + /// The registration keeps the service manager holding a strong reference + /// to the object, so it stays callable regardless of what else this + /// process drops — until it is withdrawn or this process exits. + func addService( + named name: String, + object: LocalObject, + allowIsolated: Bool = false, + dumpPriority: ServiceManager.DumpPriority = .default, + header: RPCHeader = .system + ) throws(BinderError) { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor, header: header) + request.append(name) + // Written as BINDER_TYPE_BINDER: the driver creates the node on first + // sight and hands the service manager a handle pointing back here. + request.append(object) + request.append(allowIsolated) + request.append(dumpPriority.rawValue) + + try finishAddService(request) + } + + private func finishAddService(_ request: Parcel) throws(BinderError) { + let reply = try transact( + with: .contextManager, + code: ServiceManager.Transaction.addService.rawValue, + request: request + ) + + var reader = reply.reader() + let exception: Int32 + do { + exception = try reader.readInt32() + } catch { + throw BinderError.parcel(error) + } + guard exception == 0 else { + throw BinderError.remoteException(exception) + } + } + + /// List the names of every registered service. + /// + /// - Parameter priority: which registrations to report. Defaults to all of + /// them; narrowing it silently shortens the list. + func services( + priority: ServiceManager.DumpPriority = .all, + header: RPCHeader = .system + ) throws(BinderError) -> [String] { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor, header: header) + request.append(priority.rawValue) + + let reply = try transact( + with: .contextManager, + code: ServiceManager.Transaction.listServices.rawValue, + request: request + ) + + var reader = reply.reader() + + let exception: Int32 + do { + exception = try reader.readInt32() + } catch { + throw BinderError.parcel(error) + } + guard exception == 0 else { + throw BinderError.remoteException(exception) + } + + do { + let count = try reader.readInt32() + // A null list is -1 and is not the same as an empty one, though + // both mean "nothing to report" here. + guard count > 0 else { return [] } + + var names = [String]() + names.reserveCapacity(Int(count)) + for _ in 0 ..< count { + if let name = try reader.readString() { + names.append(name) + } + } + return names + } catch { + throw BinderError.parcel(error) + } + } +} diff --git a/Sources/Binder/ThreadExit.swift b/Sources/Binder/ThreadExit.swift new file mode 100644 index 0000000..461f841 --- /dev/null +++ b/Sources/Binder/ThreadExit.swift @@ -0,0 +1,65 @@ +// +// ThreadExit.swift +// Binder +// + +import SystemPackage +import Socket +import CBinder + +internal extension Binder { + + /// The `BINDER_THREAD_EXIT` payload — a `__s32` the driver ignores. + /// + /// The command is thread-scoped with no argument of substance; the value + /// exists because every ioctl needs one. + struct ThreadExit { + + var value: Int32 = 0 + } +} + +extension Binder.ThreadExit: IOControlValue { + + static var id: BinderCommand { .threadExit } + + mutating func withUnsafeMutablePointer( + _ body: (UnsafeMutableRawPointer) throws -> (Result) + ) rethrows -> Result { + try Swift.withUnsafeMutableBytes(of: &self) { buffer in + try body(buffer.baseAddress!) + } + } +} + +public extension BinderConnection { + + /// Tell the driver the calling thread is done with binder. + /// + /// The kernel keeps a `binder_thread` for every thread that has ever + /// issued an ioctl on the device, and frees it on this command or when the + /// device closes. For threads whose exit this library controls that + /// housekeeping is automatic — ``serve(while:)`` sends it as the loop + /// unwinds. It is public for the case the library cannot see: a caller + /// driving transactions from threads it manages itself, which would + /// otherwise accumulate driver state that outlives each thread's useful + /// life. + /// + /// Scoped to the calling thread. Calling it from a thread that then + /// transacts again is harmless — the driver simply allocates fresh state — + /// so a mistimed call costs a little memory churn rather than correctness. + func exitThread() throws(BinderError) { + var value = Binder.ThreadExit() + #if ENABLE_MOCKING + assert(type(of: value).id.rawValue == BINDER_THREAD_EXIT) + #else + // do/catch inside the real branch: under mocking the block throws + // nothing, so a catch would infer `any Error` rather than Errno. + do { + try device.handle.fileDescriptor.inputOutput(&value) + } catch { + throw BinderError.system(error) + } + #endif + } +} diff --git a/Sources/Binder/Transaction+Async.swift b/Sources/Binder/Transaction+Async.swift new file mode 100644 index 0000000..82307ca --- /dev/null +++ b/Sources/Binder/Transaction+Async.swift @@ -0,0 +1,129 @@ +// +// Transaction+Async.swift +// Binder +// + +import Dispatch + +/// Async forms of the blocking operations. +/// +/// A synchronous binder transaction has no non-blocking form: the driver parks +/// the calling thread until the peer replies, and there is no way to ask it for +/// "whatever is ready now". So these are not natively asynchronous — they move +/// the blocking call onto a thread that is allowed to block, and suspend the +/// caller until it finishes. +/// +/// That distinction matters. Swift's cooperative pool has roughly one thread per +/// core and expects tasks to yield rather than block; a binder call that waits +/// on a slow service would take a core out of circulation for the duration. Any +/// number of these can be in flight at once — they run on ``BinderConnection``'s +/// own concurrent queue. +/// +/// ## Driver thread state +/// +/// The kernel keeps a `binder_thread` for every thread that has issued an ioctl +/// on the device, and frees it when that thread sends `BINDER_THREAD_EXIT` or +/// when the device is closed. Work here runs on Dispatch's worker threads, whose +/// exit this code cannot observe, so those structures accumulate — one per +/// distinct worker thread that has carried a transaction — until the connection +/// is released. Dispatch's pool is bounded, so this is bounded too, but it is +/// the reason a long-lived process should hold one connection rather than +/// opening one per call. +public extension BinderConnection { + + /// Send a transaction and suspend until the reply arrives. + /// + /// The async counterpart of + /// ``BinderConnection/transact(with:code:request:flags:)``. + /// + /// - Note: cancelling the surrounding task does not cancel the transaction. + /// Once the driver has taken it there is no way to withdraw it, and the + /// peer will reply or die; abandoning the wait would leak the reply + /// buffer, since nothing would be left to hand it back with + /// `BC_FREE_BUFFER`. + func transact( + with target: RemoteObject, + code: UInt32, + request: Parcel, + flags: TransactionFlags = .acceptFds + ) async throws(BinderError) -> Parcel { + try await detached { () throws(BinderError) -> Parcel in + try self.transact( + with: target, + code: code, + request: request, + flags: flags + ) + } + } + + /// Look up a service, suspending until it is found or the lookup gives up. + /// + /// Blocks in the *server* for up to five seconds if the service is not yet + /// registered, so this can suspend for that long. + func service( + named name: String, + header: RPCHeader = .system + ) async throws(BinderError) -> RemoteProxy? { + try await detached { () throws(BinderError) -> RemoteProxy? in + try self.service(named: name, header: header) + } + } + + /// Look up a service without waiting for it to appear. + func checkService( + named name: String, + header: RPCHeader = .system + ) async throws(BinderError) -> RemoteProxy? { + try await detached { () throws(BinderError) -> RemoteProxy? in + try self.checkService(named: name, header: header) + } + } + + /// List the names of every registered service. + func services( + priority: ServiceManager.DumpPriority = .all, + header: RPCHeader = .system + ) async throws(BinderError) -> [String] { + try await detached { () throws(BinderError) -> [String] in + try self.services(priority: priority, header: header) + } + } +} + +internal extension BinderConnection { + + /// Run a blocking binder operation off the cooperative pool. + /// + /// The result travels back as a `Result` rather than through a throwing + /// continuation so that the error type survives: a throwing continuation + /// erases to `any Error`, and rethrowing that would widen every one of + /// these signatures away from ``BinderError``. + func detached( + _ work: @escaping @Sendable () throws(BinderError) -> Success + ) async throws(BinderError) -> Success { + let outcome: Result = await withCheckedContinuation { continuation in + queue.async { + let outcome: Result + do { + outcome = .success(try work()) + } catch let error as BinderError { + outcome = .failure(error) + } catch { + // Unreachable: `work` is declared throws(BinderError), so + // nothing else can come out of it. Swift still requires the + // arm because the enclosing Dispatch closure is untyped. + preconditionFailure("unexpected error type: \(error)") + } + continuation.resume(returning: outcome) + } + } + + switch outcome { + case .success(let value): + return value + case .failure(let error): + throw error + } + } +} diff --git a/Sources/Binder/Transaction.swift b/Sources/Binder/Transaction.swift new file mode 100644 index 0000000..ee07e85 --- /dev/null +++ b/Sources/Binder/Transaction.swift @@ -0,0 +1,363 @@ +// +// Transaction.swift +// Binder +// + +import SystemPackage +import Socket +import CBinder + +public extension BinderConnection { + + /// Send a transaction to a remote object and wait for its reply. + /// + /// - Parameters: + /// - target: the object to call. ``RemoteObject/contextManager`` is + /// always reachable; anything else has to be obtained from it first. + /// - code: the interface-defined transaction code. + /// - request: the payload. + /// - flags: `acceptFds` by default, matching libbinder. Adding `oneWay` + /// makes the call return as soon as the driver has taken the + /// transaction, with an empty parcel. + /// + /// - Returns: the reply parcel, copied out of the mapped buffer. + /// + /// Any binder object in the reply arrives holding a **strong reference** + /// that this connection took on the caller's behalf. Adopt it with + /// ``adopt(_:)`` to have it released automatically, or release it by hand + /// with ``release(_:)``. Dropping the parcel without doing either leaks + /// the reference and keeps the target process alive. + /// + /// Blocks the calling thread. A synchronous transaction has no + /// non-blocking form — the driver parks the thread until the peer replies — + /// so this must not be called from a thread something else needs. + func transact( + with target: RemoteObject, + code: UInt32, + request: Parcel, + flags: TransactionFlags = .acceptFds + ) throws(BinderError) -> Parcel { + // The driver reads the request through the pointers in + // binder_transaction_data rather than from a copy, so the bytes and the + // offsets both need addresses that stay put for the whole exchange. A + // Swift array cannot promise that outside a `withUnsafe...` closure, and + // a closure would put the blocking wait somewhere errors cannot be + // typed, so both are staged in memory this function owns. + let staged = UnsafeMutableBufferPointer + .allocate(capacity: Swift.max(request.bytes.count, 1)) + defer { staged.deallocate() } + _ = staged.initialize(fromContentsOf: request.bytes) + + // The driver rewrites handles only at the positions named here. + let offsets = UnsafeMutableBufferPointer + .allocate(capacity: Swift.max(request.objectOffsets.count, 1)) + defer { offsets.deallocate() } + _ = offsets.initialize( + fromContentsOf: request.objectOffsets.map { binder_size_t($0) } + ) + + // Out-of-line buffers live outside the parcel bytes, so each payload is + // staged separately and its address written into its descriptor. The + // staged buffers must outlive the ioctl, so they are freed only after + // the exchange, not on the way out of this scope. + var stagedBuffers: [UnsafeMutableBufferPointer] = [] + defer { stagedBuffers.forEach { $0.deallocate() } } + for buffer in request.scatterGatherBuffers { + let payload = UnsafeMutableBufferPointer + .allocate(capacity: Swift.max(buffer.payload.count, 1)) + _ = payload.initialize(fromContentsOf: buffer.payload) + stagedBuffers.append(payload) + + // Overwrite the placeholder pointer in the descriptor's `buffer` + // field, at offset 8 within the binder_buffer_object. + let address = binder_uintptr_t(UInt(bitPattern: payload.baseAddress)) + withUnsafeBytes(of: address) { source in + for index in 0 ..< source.count { + staged[buffer.offset + Self.bufferObjectPointerOffset + index] = source[index] + } + } + } + + var data = binder_transaction_data() + data.target.handle = target.handle + data.cookie = 0 + data.code = code + data.flags = flags.rawValue + data.data_size = binder_size_t(request.bytes.count) + data.offsets_size = binder_size_t( + request.objectOffsets.count * MemoryLayout.size + ) + data.data.ptr.buffer = binder_uintptr_t(UInt(bitPattern: staged.baseAddress)) + data.data.ptr.offsets = binder_uintptr_t(UInt(bitPattern: offsets.baseAddress)) + + var commands = CommandStream() + if request.hasBuffers { + // BC_TRANSACTION_SG wraps the same transaction with the total space + // the driver must reserve for the out-of-line buffers. + var scatterGather = binder_transaction_data_sg() + scatterGather.transaction_data = data + scatterGather.buffers_size = binder_size_t(request.buffersSize) + commands.append(.transactionSg, payload: scatterGather) + } else { + commands.append(.transaction, payload: data) + } + + return try waitForResponse(commands, isOneWay: flags.contains(.oneWay)) + } + + /// Offset of the `buffer` pointer within a `binder_buffer_object`. + /// + /// Named rather than literal because it is written into staged bytes by + /// hand, where a wrong value corrupts the descriptor silently. + fileprivate static var bufferObjectPointerOffset: Int { 8 } +} + +internal extension BinderConnection { + + /// Drive the exchange until a command arrives that ends it. + /// + /// The driver does not answer a transaction with a reply; it answers with a + /// stream in which the reply may be several commands in, or may not have + /// arrived yet at all. Bookkeeping commands are consumed and ignored here + /// rather than surfaced, because a caller waiting on a reply has nothing + /// useful to do with `BR_NOOP`. + func waitForResponse( + _ commands: CommandStream, + isOneWay: Bool + ) throws(BinderError) -> Parcel { + // libbinder reads into 256 bytes. A reply's payload does not travel + // here — it lands in the mapped buffer and only its descriptor comes + // through this stream — so this holds commands, not data. + let readBuffer = UnsafeMutableBufferPointer.allocate(capacity: 256) + defer { readBuffer.deallocate() } + readBuffer.initialize(repeating: 0) + + var pending = commands + + while true { + let returned = try talkWithDriver(&pending, into: readBuffer) + var stream = ReturnStream(bytes: returned) + + while true { + let element: ReturnStream.Element? + do { + element = try stream.next() + } catch { + throw BinderError.stream(error) + } + guard let element else { break } + + switch element.command { + case .noop, .spawnLooper, .onewaySpamSuspect, + .transactionPendingFrozen: + // Bookkeeping. BR_SPAWN_LOOPER is the driver asking for + // another service thread; ignoring it is correct for a + // client that serves nothing. + continue + + case .transactionComplete: + // The driver has taken the transaction. For a one-way call + // that is the whole exchange. + if isOneWay { + return Parcel() + } + + case .deadReply: + throw BinderError.deadObject + + case .failedReply: + throw BinderError.failedTransaction + + case .frozenReply: + throw BinderError.frozen + + case .error: + throw BinderError.driver(element.decode(as: Int32.self) ?? 0) + + case .reply: + guard let data = element.decode(as: binder_transaction_data.self) else { + throw BinderError.unexpectedReturn(element.command) + } + let reply = try copyReply(data) + // Acquiring and freeing go out together, acquires first. + // + // The driver ties the reference it granted for each object + // in this reply to the buffer, and drops it in + // binder_transaction_buffer_release. So a handle decoded + // from the reply is already dead if the buffer went back + // first — and worse than dead, because the driver is free + // to reissue that number for a different object. + // + // The payload also stays checked out of the mapping until + // the free. Skipping it leaks the mapping one transaction + // at a time, until the process can no longer receive + // anything at all. + try adoptObjectsAndFree(in: reply, buffer: data.data.ptr.buffer) + return reply + + default: + throw BinderError.unexpectedReturn(element.command) + } + } + } + } + + /// One `BINDER_WRITE_READ`: hand over pending commands, take back returns. + func talkWithDriver( + _ commands: inout CommandStream, + into readBuffer: UnsafeMutableBufferPointer + ) throws(BinderError) -> [UInt8] { + let staged = UnsafeMutableBufferPointer + .allocate(capacity: Swift.max(commands.count, 1)) + defer { staged.deallocate() } + _ = staged.initialize(fromContentsOf: commands.bytes) + + var transaction = WriteReadTransaction() + transaction.write_size = binder_size_t(commands.count) + transaction.write_consumed = 0 + transaction.write_buffer = binder_uintptr_t(UInt(bitPattern: staged.baseAddress)) + transaction.read_size = binder_size_t(readBuffer.count) + transaction.read_consumed = 0 + transaction.read_buffer = binder_uintptr_t(UInt(bitPattern: readBuffer.baseAddress)) + + do { + try device.handle.inputOutput(&transaction) + } catch { + throw BinderError.system(error) + } + + // Anything the driver took must not be sent again. It reports this + // separately from write_size precisely because it is allowed to take + // less than was offered. + let consumed = Int(transaction.write_consumed) + if consumed >= commands.count { + commands = CommandStream() + } else if consumed > 0 { + commands.removeFirst(consumed) + } + + let count = Int(transaction.read_consumed) + guard count <= readBuffer.count else { + throw BinderError.replyOutOfBounds + } + return Array(UnsafeRawBufferPointer(rebasing: UnsafeRawBufferPointer(readBuffer)[0 ..< count])) + } + + /// Copy a reply's payload out of the mapped buffer. + /// + /// The driver hands back a pointer into the mapping rather than a copy, so + /// the bytes are valid only until the buffer is released. Copying here + /// means a returned ``Parcel`` outlives the transaction that produced it, + /// which is what a caller expects of a value type. + func copyReply(_ data: binder_transaction_data) throws(BinderError) -> Parcel { + let size = Int(data.data_size) + guard size >= 0 else { + throw BinderError.replyOutOfBounds + } + guard size > 0 else { + return Parcel() + } + + let payload = try copyFromMapping( + at: data.data.ptr.buffer, + count: size, + as: UInt8.self + ) + + // The offsets array lives in the mapping too, alongside the payload. + let offsetCount = Int(data.offsets_size) / MemoryLayout.size + var offsets: [Int] = [] + if offsetCount > 0 { + let raw = try copyFromMapping( + at: data.data.ptr.offsets, + count: offsetCount, + as: binder_size_t.self + ) + // An offset naming a position outside the payload would send the + // object reader off the end, so they are filtered here rather than + // trusted — this array came from another process. + offsets = raw + .map { Int($0) } + .filter { $0 >= 0 && $0 + MemoryLayout.size <= size } + } + + // A received buffer object's descriptor points into the mapping, which + // is about to be released, so each out-of-line payload is copied out + // now — the same reason the inline bytes are. Without this a reader + // would follow a dangling pointer into freed shared memory. + var buffers: [(offset: Int, payload: [UInt8])] = [] + for offset in offsets { + guard offset + MemoryLayout.size <= size else { continue } + let descriptor = Array(payload[offset ..< offset + MemoryLayout.size]) + .withUnsafeBytes { $0.loadUnaligned(as: binder_buffer_object.self) } + guard descriptor.hdr.type == BinderType.pointer.rawValue else { continue } + + let length = Int(descriptor.length) + let data = try copyFromMapping(at: descriptor.buffer, count: length, as: UInt8.self) + buffers.append((offset: offset, payload: data)) + } + + return Parcel(bytes: payload, objectOffsets: offsets, scatterGatherBuffers: buffers) + } + + /// Copy a run of values out of the mapped region, bounds-checked. + /// + /// Every descriptor checked here arrives from another process, so none of + /// it is trusted: a pointer outside the mapping would mean reading unmapped + /// memory, and is refused rather than clamped. + private func copyFromMapping( + at address: binder_uintptr_t, + count: Int, + as type: T.Type + ) throws(BinderError) -> [T] { + let size = count * MemoryLayout.size + let start = UInt(address) + let mapping = UInt(bitPattern: buffer.address) + + guard count >= 0, + start >= mapping, + start &+ UInt(size) <= mapping &+ UInt(buffer.size), + let pointer = UnsafeRawPointer(bitPattern: start) else { + throw BinderError.replyOutOfBounds + } + + let raw = UnsafeRawBufferPointer(start: pointer, count: size) + return (0 ..< count).map { + raw.loadUnaligned(fromByteOffset: $0 * MemoryLayout.size, as: T.self) + } + } + + /// Take a strong reference to each object in a reply, then hand the buffer + /// back. + /// + /// One command stream, acquires ahead of the free, because the ordering is + /// the whole point: the driver applies them in the order it consumes them, + /// and the free is what drops the references the acquires are replacing. + func adoptObjectsAndFree( + in reply: Parcel, + buffer pointer: binder_uintptr_t + ) throws(BinderError) { + var commands = CommandStream() + for object in reply.objects { + commands.append(.acquire, payload: object.handle) + } + commands.append(.freeBuffer, payload: pointer) + + let readBuffer = UnsafeMutableBufferPointer.allocate(capacity: 256) + defer { readBuffer.deallocate() } + readBuffer.initialize(repeating: 0) + + // Sent now rather than batched onto the next transaction: holding it + // back would keep the mapping checked out indefinitely if the caller + // never transacts again. + _ = try talkWithDriver(&commands, into: readBuffer) + } +} + +internal extension CommandStream { + + /// Drop commands the driver has already consumed. + mutating func removeFirst(_ count: Int) { + self = CommandStream(bytes: Array(bytes.dropFirst(count))) + } +} diff --git a/Sources/Binder/WeakProxy.swift b/Sources/Binder/WeakProxy.swift new file mode 100644 index 0000000..cc9d69c --- /dev/null +++ b/Sources/Binder/WeakProxy.swift @@ -0,0 +1,91 @@ +// +// WeakProxy.swift +// Binder +// + +/// A weak reference to a remote object. +/// +/// The `wp<>` to ``RemoteProxy``'s `sp<>`. A weak reference keeps the *handle* +/// from being recycled — so the number will not silently come to mean a +/// different object — but does not keep the target alive. It is what you hold +/// for something you want to reach if it is still there, without being the +/// reason it stays there: a cache of proxies, a back-reference, an observer that +/// should not prevent teardown. +/// +/// The only thing you can do with a weak reference is try to ``promote()`` it to +/// a strong one. If the target is still alive, that yields a ``RemoteProxy`` +/// that keeps it alive for as long as it is held; if the target is gone, it +/// yields `nil`. +/// +/// ## Promotion is a snapshot, not a subscription +/// +/// A successful ``promote()`` means the object was alive at that instant. It can +/// die immediately after, so a promoted proxy is not a guarantee of liveness for +/// any later call — those can still fail with ``BinderError/deadObject``. +/// ``BinderConnection/notifyOnDeath(of:handler:)`` is the mechanism that tells +/// you *when* it dies; promotion only tells you whether it is dead *now*. +public final class WeakProxy { + + /// The object this refers to. + public let object: RemoteObject + + private let connection: BinderConnection + + internal init(object: RemoteObject, connection: BinderConnection) { + self.object = object + self.connection = connection + } + + deinit { + // Release the weak reference taken when this was created. A failure + // means the driver already dropped it, which is an accounting bug in + // this type rather than a runtime condition; deinit cannot throw. + try? connection.decrementWeak(object) + } +} + +extension WeakProxy: @unchecked Sendable {} + +public extension WeakProxy { + + /// The handle the driver assigned. + var handle: UInt32 { + object.handle + } + + /// Try to obtain a strong reference to the target. + /// + /// - Returns: an owning ``RemoteProxy`` if the object is still alive, or + /// `nil` if it has died. + /// + /// Attempts to take a strong reference. The driver grants it only while the + /// target node still exists, so a rejection is read as "gone" rather than as + /// an error — anything else the driver reports is surfaced as thrown. + func promote() throws(BinderError) -> RemoteProxy? { + do { + try connection.acquire(object) + } catch .deadObject, .failedTransaction { + // The node is gone. The weak reference this proxy holds is still + // valid — the handle is not recycled until it too is dropped — so + // there is nothing to undo here. + return nil + } + return connection.adopt(object) + } +} + +public extension BinderConnection { + + /// Take a weak reference to an object and own it. + /// + /// The object must be one this process already holds a reference to — a + /// live ``RemoteProxy``'s ``RemoteProxy/object`` — since the driver rejects + /// a weak increment on a handle the process does not already hold. + /// + /// The returned proxy holds the weak reference for its lifetime and drops it + /// on `deinit`. + func weakReference(to object: RemoteObject) throws(BinderError) -> WeakProxy { + try incrementWeak(object) + return WeakProxy(object: object, connection: self) + } +} diff --git a/Sources/CBinder/include/binder.h b/Sources/CBinder/include/binder.h index 999b10d..1519f48 100644 --- a/Sources/CBinder/include/binder.h +++ b/Sources/CBinder/include/binder.h @@ -33,7 +33,13 @@ #endif -#ifdef __APPLE__ +// The kernel uapi headers (, ) ship with glibc +// and Bionic but not with musl, and never on Apple. So a libc without them — +// Apple, or a Linux libc that is neither glibc nor Bionic, which in practice +// means musl — gets the kernel integer types defined here, and takes the ioctl +// macros from instead. glibc and Bionic keep using the real +// kernel headers. +#if defined(__APPLE__) || (defined(__linux__) && !defined(__GLIBC__) && !defined(__BIONIC__)) typedef uint8_t __u8; typedef uint16_t __u16; @@ -48,6 +54,11 @@ typedef int64_t __s64; typedef pid_t __kernel_pid_t; typedef uid_t __kernel_uid32_t; +#if !defined(__APPLE__) +// musl: _IOW / _IOR / _IOWR come from here rather than . +#include +#endif + #else #include diff --git a/Sources/CBinder/include/linux/android/binder.h b/Sources/CBinder/include/linux/android/binder.h index 65ab2fa..ab85278 100644 --- a/Sources/CBinder/include/linux/android/binder.h +++ b/Sources/CBinder/include/linux/android/binder.h @@ -6,7 +6,12 @@ */ #ifndef _UAPI_LINUX_BINDER_H #define _UAPI_LINUX_BINDER_H -#if __linux__ +// Modified from the auto-generated original: the kernel-header include is +// gated on a libc that actually ships them (glibc or Bionic), so musl - which +// is __linux__ but has no - falls to the stdint path. The +// enclosing Sources/CBinder/include/binder.h defines the __uN types and pulls +// in the ioctl macros for that case. +#if defined(__linux__) && (defined(__GLIBC__) || defined(__BIONIC__)) #include #include #else diff --git a/Sources/CBinder/include/linux/android/binderfs.h b/Sources/CBinder/include/linux/android/binderfs.h index b109b45..4b073b9 100644 --- a/Sources/CBinder/include/linux/android/binderfs.h +++ b/Sources/CBinder/include/linux/android/binderfs.h @@ -6,8 +6,13 @@ */ #ifndef _UAPI_LINUX_BINDERFS_H #define _UAPI_LINUX_BINDERFS_H +// Modified from the auto-generated original for musl, as in binder.h: the +// vendored binder.h is included on any Linux (it defines the __uN types for +// musl itself), but the kernel uapi headers are gated on glibc or Bionic. #if __linux__ #include +#endif +#if defined(__linux__) && (defined(__GLIBC__) || defined(__BIONIC__)) #include #include #endif diff --git a/Tests/BinderTests/BinderConnectionTests.swift b/Tests/BinderTests/BinderConnectionTests.swift new file mode 100644 index 0000000..a9cb874 --- /dev/null +++ b/Tests/BinderTests/BinderConnectionTests.swift @@ -0,0 +1,179 @@ +// +// BinderConnectionTests.swift +// Binder +// + +import SystemPackage +import Testing + +@testable import Binder + +/// What can be checked about a connection without a binder device present. +/// +/// Opening one requires a driver, so these are the properties that hold before +/// any device is involved: the type's shape, and the constants a caller can +/// read without connecting. +@Suite +struct BinderConnectionTests { + + /// A connection is shared across tasks rather than copied into each, and + /// the async operations hand work to another thread through an escaping + /// closure. Both of those stop compiling if it goes back to being a + /// non-copyable value, so the requirement is pinned here rather than left + /// to be rediscovered. + @Test func connectionIsSendable() { + func requireSendable(_: T.Type) {} + requireSendable(BinderConnection.self) + } + + @Test func exposesTheMappedBufferSize() { + #expect(BinderConnection.defaultBufferSize == MappedBuffer.defaultSize) + #expect(BinderConnection.defaultBufferSize > 0) + } + + /// libbinder's DEFAULT_MAX_BINDER_THREADS. Services on the other end are + /// provisioned expecting it. + @Test func defaultMaxThreadsMatchesLibbinder() { + #expect(Binder.defaultMaxThreads == 15) + } + + @Test func defaultPathIsTheBinderDevice() { + #expect(Binder.path == "/dev/binder") + } +} + +/// Never called — it exists to be compiled. +/// +/// Opening a connection needs a driver, so the async surface cannot be +/// exercised at runtime here. What can still be checked is that it type-checks +/// from a caller's position: that the async overloads are reachable, that their +/// errors stay typed as `BinderError` rather than widening to `any Error`, and +/// that a connection can be held across a suspension point without tripping +/// sendability. Each of those has already broken once while this was being +/// written. +private func asyncSurfaceCompiles(_ connection: BinderConnection) async { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append("activity") + + do { + _ = try await connection.service(named: "activity") + _ = try await connection.checkService(named: "activity") + _ = try await connection.services() + _ = try await connection.transact(with: .contextManager, code: 1, request: request) + + // A looked-up service is an owning reference and is callable directly, + // with no handle unwrapping at the call site. + if let service = try await connection.service(named: "activity") { + _ = try await service.transact(code: 1, request: request) + // The async call helper returns a whole parcel; results come from + // resultReader(), the header already consumed. + let reply = try await service.call(1, interface: "com.example.IThing") { + $0.append("arg") + } + _ = reply.resultReader() + } + } catch { + // Typed: if this ever widens to `any Error`, the pattern stops + // compiling and the async signatures have silently lost their type. + let typed: BinderError = error + _ = typed + } +} + +/// Likewise for the synchronous surface, which the async one delegates to. +/// +/// Adding async overloads with the same argument labels risks making the plain +/// calls ambiguous; this fails to compile if that happens. +private func syncSurfaceCompiles(_ connection: BinderConnection) { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + + do { + _ = try connection.service(named: "activity") + _ = try connection.checkService(named: "activity") + _ = try connection.services() + _ = try connection.transact(with: .contextManager, code: 1, request: request) + + if let service = try connection.checkService(named: "activity") { + _ = try service.transact(code: 1, request: request) + } + + let object = RemoteObject(handle: 1) + try connection.acquire(object) + try connection.release(object) + try connection.incrementWeak(object) + try connection.decrementWeak(object) + _ = connection.adopt(object) + _ = try connection.retain(object) + try connection.addService(named: "example", object: object) + + // Hosting an object and serving it. The handler is @Sendable because + // several threads may serve one connection concurrently. + let hosted = connection.makeLocalObject(interface: "com.example.IThing") { transaction in + guard transaction.senderUserID == 1000 else { return nil } + var reply = Parcel() + reply.append(Int32(0)) + return reply + } + var registration = Parcel() + registration.append(hosted) + + connection.stopServing() + _ = connection.isServingTransactions + try connection.serve(while: { false }) + connection.withdraw(hosted) + + // Liveness, and the standing watch that does not race with it. + _ = try connection.ping(object) + _ = connection.isServiceManagerPresent + + let watch = try connection.notifyOnDeath(of: object) { + // Runs on a serving thread, hence @Sendable. + } + try connection.cancel(watch) + + // Publishing a hosted object, and the manual thread-exit for a caller + // driving transactions from threads it owns. + try connection.addService(named: "com.example.thing", object: hosted) + try connection.exitThread() + + // Weak references and promotion. + let strong = connection.adopt(object) + let weakRef = try strong.weak() + if let promoted = try weakRef.promote() { + _ = try promoted.transact(code: 1, request: request) + } + _ = try connection.weakReference(to: object) + + // File-descriptor passing (writing does not throw) and the failure + // diagnostic. Reading an fd throws ParcelError and is exercised in + // FileDescriptorObjectTests, so it is kept out of this BinderError block. + var withFd = Parcel() + withFd.append(fileDescriptor: FileDescriptor(rawValue: 1)) + _ = try connection.lastError() + + // A scatter-gather request: an inline header plus an out-of-line + // buffer, sent through the same transact entry point. + var sgRequest = Parcel() + sgRequest.appendInterfaceToken("android.hardware.foo@1.0::IFoo") + sgRequest.append(buffer: [0xAA, 0xBB]) + _ = try connection.transact(with: object, code: 1, request: sgRequest) + + // The call helpers, which write the envelope so a method supplies only + // its code and arguments. Reading from the returned reader throws + // ParcelError and is exercised in CallTests, so it is kept out of this + // BinderError block. + let proxy = connection.adopt(object) + _ = try proxy.call(1, interface: "com.example.IThing") { $0.append("arg") } + try proxy.post(2, interface: "com.example.IThing") { $0.append(Int32(1)) } + + // Becoming the context manager, the one binder role beyond calling and + // serving. + try connection.becomeContextManager() + try connection.becomeContextManager(securityContext: true) + } catch { + let typed: BinderError = error + _ = typed + } +} diff --git a/Tests/BinderTests/BinderTests.swift b/Tests/BinderTests/BinderTests.swift index 8a19593..acf5f47 100644 --- a/Tests/BinderTests/BinderTests.swift +++ b/Tests/BinderTests/BinderTests.swift @@ -5,15 +5,37 @@ import CBinder @Suite struct BinderTests { - - @Test func device() throws { + + /// Whether these tests can reach a binder device. + /// + /// Mocking is compiled in on macOS, so the device path is never touched + /// there. Everywhere else these two tests need a real driver, and a Linux + /// host without the binder module loaded has no /dev/binder — which is the + /// common case for a development machine and for CI. Reporting that as a + /// failure trains people to ignore a red suite; skipping says what is + /// actually true. + static var hasDevice: Bool { + #if ENABLE_MOCKING + return true + #else + guard let device = try? FileDescriptor.open(Binder.path, .readOnly) else { + return false + } + try? device.close() + return true + #endif + } + + @Test(.enabled(if: BinderTests.hasDevice, "no binder device at \(Binder.path)")) + func device() throws { let device = try Binder() #if ENABLE_MOCKING #expect(device.handle.fileDescriptor.rawValue == 0) #endif } - - @Test func version() throws { + + @Test(.enabled(if: BinderTests.hasDevice, "no binder device at \(Binder.path)")) + func version() throws { let version = try BinderVersion.current print("Binder version:", version) #expect(version == BinderVersion.compiledVersion) diff --git a/Tests/BinderTests/BufferObjectTests.swift b/Tests/BinderTests/BufferObjectTests.swift new file mode 100644 index 0000000..9990e58 --- /dev/null +++ b/Tests/BinderTests/BufferObjectTests.swift @@ -0,0 +1,161 @@ +// +// BufferObjectTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +@Suite +struct BufferObjectTests { + + // MARK: - Layout + + /// binder_buffer_object is 40 bytes with the out-of-line pointer at offset + /// 8. The transaction path writes that pointer into staged bytes by hand, + /// so a shift here would corrupt every buffer silently. + @Test func bufferObjectLayoutIsStable() { + #expect(MemoryLayout.size == 40) + #expect(MemoryLayout.size + == MemoryLayout.size + + MemoryLayout.size) + #expect(DriverCommandProtocol.transactionSg.payloadSize + == MemoryLayout.size) + #expect(DriverCommandProtocol.replySg.payloadSize + == MemoryLayout.size) + } + + // MARK: - Writing + + /// A buffer descriptor is written inline as BINDER_TYPE_PTR, at a declared + /// offset, with the payload held out of band and a placeholder pointer. + @Test func writesADescriptorWithAPlaceholderPointer() { + var parcel = Parcel() + parcel.append(buffer: [1, 2, 3, 4, 5]) + + #expect(parcel.hasBuffers) + #expect(parcel.objectOffsets == [0]) + #expect(parcel.count == MemoryLayout.size) + + let object = parcel.bytes.withUnsafeBytes { + $0.loadUnaligned(as: binder_buffer_object.self) + } + #expect(object.hdr.type == BinderType.pointer.rawValue) + #expect(object.length == 5) + // The pointer is filled in only at send time. + #expect(object.buffer == 0) + } + + /// The inline descriptor is fixed size; the payload does not inflate it. + @Test func payloadDoesNotAffectTheInlineSize() { + var small = Parcel() + small.append(buffer: [0]) + var large = Parcel() + large.append(buffer: [UInt8](repeating: 0, count: 4096)) + #expect(small.count == large.count) + } + + /// A buffer object is not a remote reference, so reference-count enumeration + /// must not turn it up — acquiring or releasing it would be meaningless. + @Test func buffersAreNotRemoteObjects() { + var parcel = Parcel() + parcel.append(buffer: [1, 2, 3]) + #expect(parcel.objects.isEmpty) + } + + // MARK: - buffers_size + + /// The reserved size is the sum of 8-aligned payload lengths, not the raw + /// sum. Reserving the raw sum leaves the driver short for every buffer but + /// the last. + @Test func buffersSizeSumsAlignedLengths() { + var parcel = Parcel() + parcel.append(buffer: [UInt8](repeating: 0, count: 5)) // -> 8 + parcel.append(buffer: [UInt8](repeating: 0, count: 8)) // -> 8 + parcel.append(buffer: [UInt8](repeating: 0, count: 1)) // -> 8 + #expect(parcel.buffersSize == 24) + } + + @Test func emptyBufferStillReservesNothingButCounts() { + var parcel = Parcel() + parcel.append(buffer: []) + #expect(parcel.hasBuffers) + #expect(parcel.buffersSize == 0) + } + + @Test(arguments: [(0, 0), (1, 8), (7, 8), (8, 8), (9, 16), (16, 16), (17, 24)]) + func alignsToEightBytes(_ length: Int, _ expected: Int) { + #expect(Parcel.bufferAlignedSize(length) == expected) + } + + // MARK: - Mixed parcels + + /// A realistic HIDL-shaped request: an inline header, then a vector as a + /// buffer. Both the object offset and the buffer are tracked, and a plain + /// parcel without buffers is not sent down the SG path. + @Test func tracksInlineDataAlongsideABuffer() { + var parcel = Parcel() + parcel.appendInterfaceToken("android.hardware.foo@1.0::IFoo") + parcel.append(Int32(1)) + let headerSize = parcel.count + parcel.append(buffer: [0xAA, 0xBB, 0xCC]) + + #expect(parcel.hasBuffers) + #expect(parcel.objectOffsets == [headerSize]) + #expect(parcel.buffersSize == 8) + } + + @Test func plainParcelDoesNotUseScatterGather() { + var parcel = Parcel() + parcel.append(Int32(1)) + parcel.append("two") + #expect(parcel.hasBuffers == false) + #expect(parcel.buffersSize == 0) + } + + // MARK: - Reading a received buffer + + /// A received parcel carries its buffer payloads out of band, since the + /// descriptor's pointer refers to freed shared memory by the time a reader + /// runs. readBuffer returns the captured bytes. + @Test func readsAReceivedBufferPayload() throws { + // Build the parcel the way copyReply does: a descriptor inline, the + // payload captured separately at the same offset. + var descriptorParcel = Parcel() + descriptorParcel.append(buffer: [0xDE, 0xAD, 0xBE, 0xEF]) + let received = Parcel( + bytes: descriptorParcel.bytes, + objectOffsets: descriptorParcel.objectOffsets, + scatterGatherBuffers: [(offset: 0, payload: [0xDE, 0xAD, 0xBE, 0xEF])] + ) + + var reader = received.reader() + #expect(try reader.readBuffer(from: received) == [0xDE, 0xAD, 0xBE, 0xEF]) + #expect(reader.isAtEnd) + } + + /// Reading a buffer where the object at the cursor is not one returns nil + /// rather than a wrong answer. + @Test func readBufferRejectsANonBuffer() throws { + var parcel = Parcel() + parcel.append(RemoteObject(handle: 1)) + var reader = parcel.reader() + #expect(try reader.readBuffer(from: parcel) == nil) + } + + /// Equality and hashing fold in the out-of-line payloads, so two parcels + /// with identical descriptors but different buffer contents are distinct. + @Test func equalityIncludesBufferPayloads() { + var a = Parcel() + a.append(buffer: [1, 2, 3]) + var b = Parcel() + b.append(buffer: [1, 2, 3]) + var c = Parcel() + c.append(buffer: [9, 9, 9]) + + #expect(a == b) + #expect(a != c) + } +} diff --git a/Tests/BinderTests/CallTests.swift b/Tests/BinderTests/CallTests.swift new file mode 100644 index 0000000..f4150fb --- /dev/null +++ b/Tests/BinderTests/CallTests.swift @@ -0,0 +1,154 @@ +// +// CallTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +/// The call helpers, exercised as a full round trip between the two sides. +/// +/// The value they add is that the envelope — interface token, exception header, +/// reader positioning — is written once rather than at every method. So the +/// tests drive both halves through the helpers and check that a request built by +/// `call`'s framing decodes through `arguments`, and a reply built by `reply` +/// decodes through what `call` leaves the caller. +@Suite +struct CallTests { + + // MARK: - Request framing + + /// `call` writes the interface token before the caller's arguments, so a + /// request it builds is what `arguments` expects to read. + @Test func callFramesARequestThatArgumentsCanRead() throws { + // The request half of RemoteProxy.call, without a connection: the same + // parcel it would build. + var request = Parcel() + request.appendInterfaceToken("com.example.IThing") + request.append(Int32(7)) + request.append("widget") + + // The serving half. + let transaction = LocalObject.Transaction( + code: 1, + request: request, + flags: .acceptFds, + senderProcessID: 1, + senderUserID: 1000 + ) + var reader = try transaction.arguments(interface: "com.example.IThing") + #expect(try reader.readInt32() == 7) + #expect(try reader.readString() == "widget") + #expect(reader.isAtEnd) + } + + /// A request for the wrong interface is rejected before any argument is + /// read, and distinctly from a build-marker mismatch. + @Test func argumentsRejectsTheWrongInterface() { + var request = Parcel() + request.appendInterfaceToken("com.example.IOther") + request.append(Int32(1)) + + let transaction = LocalObject.Transaction( + code: 1, request: request, flags: [], + senderProcessID: 1, senderUserID: 1000 + ) + #expect(throws: ParcelError.wrongInterface( + expected: "com.example.IThing", + found: "com.example.IOther" + )) { + try transaction.arguments(interface: "com.example.IThing") + } + } + + // MARK: - Reply framing + + /// `reply` writes the EX_NONE header before results; `resultReader` skips it + /// so the caller reads results directly — the two are matched. + @Test func replyAndResultReaderAreMatched() throws { + let reply = Parcel.reply { + $0.append("ok") + $0.append(Int32(14)) + } + + var reader = reply.resultReader() + #expect(try reader.readString() == "ok") + #expect(try reader.readInt32() == 14) + #expect(reader.isAtEnd) + } + + /// The success header is present and zero. + @Test func replyOpensWithANoneException() throws { + let reply = Parcel.reply { $0.append(Int32(99)) } + var reader = reply.reader() + #expect(try reader.readInt32() == 0) // EX_NONE + #expect(try reader.readInt32() == 99) + } + + @Test func exceptionReplyCarriesTheCode() throws { + let reply = Parcel.exception(-8) // EX_SERVICE_SPECIFIC + var reader = reply.reader() + #expect(try reader.readInt32() == -8) + } + + // MARK: - A whole exchange through the helpers + + /// Build the request as `call` frames it, dispatch through a handler that + /// uses `arguments` and `reply`, and read the result as `call` leaves it — + /// the envelope handled entirely by the helpers on both sides. + @Test func roundTripsThroughTheHelpers() throws { + let registry = LocalObjectRegistry() + let echo = registry.register(interfaceDescriptor: "com.example.IEcho") { transaction in + do { + var args = try transaction.arguments(interface: "com.example.IEcho") + let message = try args.readString() ?? "" + return Parcel.reply { $0.append("echo:\(message)") } + } catch { + return Parcel.exception(-1) + } + } + + // Request framing, as RemoteProxy.call builds it. + var request = Parcel() + request.appendInterfaceToken("com.example.IEcho") + request.append("hi") + + let transaction = LocalObject.Transaction( + code: 1, request: request, flags: .acceptFds, + senderProcessID: 1, senderUserID: 1000 + ) + let reply = try #require(try echo.handler(transaction)) + + // Result reading, as RemoteProxy.call leaves it: header consumed. + var reader = reply.resultReader() + #expect(try reader.readString() == "echo:hi") + #expect(reader.isAtEnd) + } + + /// A handler that rejects the interface answers with an exception the caller + /// side surfaces, rather than a decode error. + @Test func mismatchedInterfaceBecomesAnExceptionReply() throws { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "com.example.IEcho") { transaction in + do { + _ = try transaction.arguments(interface: "com.example.IEcho") + return Parcel.reply() + } catch { + return Parcel.exception(-1) + } + } + + var request = Parcel() + request.appendInterfaceToken("com.example.IWrong") + + let transaction = LocalObject.Transaction( + code: 1, request: request, flags: [], + senderProcessID: 1, senderUserID: 1000 + ) + let reply = try #require(try object.handler(transaction)) + var reader = reply.reader() + #expect(try reader.readInt32() == -1) + } +} diff --git a/Tests/BinderTests/ContextManagerTests.swift b/Tests/BinderTests/ContextManagerTests.swift new file mode 100644 index 0000000..c25303d --- /dev/null +++ b/Tests/BinderTests/ContextManagerTests.swift @@ -0,0 +1,51 @@ +// +// ContextManagerTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +@Suite +struct ContextManagerTests { + + /// The plain registration is a legacy 4-byte int; the security-context form + /// carries a full flat_binder_object. Sending one where the driver expects + /// the other is a size mismatch it rejects. + @Test func registrationPayloadsMatchTheirIoctls() { + #expect(BinderCommand.setContextMgr.rawValue + == numericCast(BINDER_SET_CONTEXT_MGR)) + #expect(BinderCommand.setContextMgrExt.rawValue + == numericCast(BINDER_SET_CONTEXT_MGR_EXT)) + #expect(MemoryLayout.size == 24) + } + + /// The two forms are distinct ioctls, so a security-context registration is + /// not silently sent as a plain one. + @Test func plainAndExtendedAreDistinct() { + #expect(BinderCommand.setContextMgr != BinderCommand.setContextMgrExt) + #expect(BinderCommand.setContextMgr.rawValue + != BinderCommand.setContextMgrExt.rawValue) + } + + /// The security-context node carries FLAT_BINDER_FLAG_TXN_SECURITY_CTX, + /// which is what asks the driver to deliver BR_TRANSACTION_SEC_CTX — the + /// same variant the serving loop now decodes. + @Test func securityContextFlagIsSet() { + #expect(FlatBinderObjectFlags.txnSecurityCtx.rawValue == 0x1000) + + var object = flat_binder_object() + object.hdr = binder_object_header(type: .binder) + object.flags = FlatBinderObjectFlags.txnSecurityCtx.rawValue + #expect(FlatBinderObjectFlags(rawValue: object.flags) + .contains(.txnSecurityCtx)) + } + + /// A context manager is reached at handle zero, the same fixed point a + /// client looks up services through — the two ends of the same protocol. + @Test func contextManagerIsHandleZero() { + #expect(RemoteObject.contextManager.handle == 0) + } +} diff --git a/Tests/BinderTests/DeathNotificationTests.swift b/Tests/BinderTests/DeathNotificationTests.swift new file mode 100644 index 0000000..e6c9536 --- /dev/null +++ b/Tests/BinderTests/DeathNotificationTests.swift @@ -0,0 +1,170 @@ +// +// DeathNotificationTests.swift +// Binder +// + +import CBinder +import Synchronization +import Testing + +@testable import Binder + +@Suite +struct DeathNotificationTests { + + // MARK: - Payload layout + + /// `binder_handle_cookie` is declared `__attribute__((packed))`: a 4-byte + /// handle followed immediately by an 8-byte cookie, 12 bytes rather than + /// the 16 alignment would otherwise produce. If Swift ever imports it + /// unpacked, every registration sends the driver a structure it reads + /// differently — and the mismatch is silent, because the command still has + /// a plausible shape. + @Test func handleCookieIsPacked() { + #expect(MemoryLayout.size == 12) + #expect(DriverCommandProtocol.requestDeathNotification.payloadSize == 12) + #expect(DriverCommandProtocol.clearDeathNotification.payloadSize == 12) + #expect(MemoryLayout.size + == DriverCommandProtocol.requestDeathNotification.payloadSize) + } + + @Test func deathCommandsCarryABareCookie() { + #expect(DriverReturnProtocol.deadBinder.payloadSize + == MemoryLayout.size) + #expect(DriverReturnProtocol.clearDeathNotificationDone.payloadSize + == MemoryLayout.size) + #expect(DriverCommandProtocol.deadBinderDone.payloadSize + == MemoryLayout.size) + } + + @Test func encodesARegistration() { + var payload = binder_handle_cookie() + payload.handle = 7 + payload.cookie = 0x1234_5678 + + var stream = CommandStream() + stream.append(.requestDeathNotification, payload: payload) + #expect(stream.count == 4 + 12) + + let decoded = Array(stream.bytes.dropFirst(4)) + .withUnsafeBytes { $0.loadUnaligned(as: binder_handle_cookie.self) } + #expect(decoded.handle == 7) + #expect(decoded.cookie == 0x1234_5678) + } + + // MARK: - Registry + + @Test func registersAndFindsANotification() { + let registry = DeathRegistry() + let notification = registry.register(object: RemoteObject(handle: 3)) { } + + #expect(registry.count == 1) + #expect(registry.notification(for: notification.cookie) === notification) + #expect(notification.object == RemoteObject(handle: 3)) + } + + /// Zero stays available as "no cookie". + @Test func cookiesStartAtOne() { + let registry = DeathRegistry() + #expect(registry.register(object: RemoteObject(handle: 1)) { }.cookie == 1) + } + + /// The same object can be watched several times, and cancelling one watch + /// must not cancel the others — which is why registration is keyed on a + /// cookie rather than on the handle. + @Test func watchesOnTheSameObjectAreIndependent() { + let registry = DeathRegistry() + let object = RemoteObject(handle: 5) + let first = registry.register(object: object) { } + let second = registry.register(object: object) { } + + #expect(first.cookie != second.cookie) + #expect(registry.count == 2) + + _ = registry.remove(cookie: first.cookie) + #expect(registry.notification(for: second.cookie) === second) + #expect(registry.count == 1) + } + + @Test func cookiesAreNotReusedAfterRemoval() { + let registry = DeathRegistry() + let first = registry.register(object: RemoteObject(handle: 1)) { } + _ = registry.remove(cookie: first.cookie) + let second = registry.register(object: RemoteObject(handle: 1)) { } + + #expect(second.cookie != first.cookie) + } + + @Test func registrationIsSafeUnderConcurrency() async { + let registry = DeathRegistry() + + await withTaskGroup(of: UInt64.self) { group in + for handle in 0 ..< 100 { + group.addTask { + registry.register(object: RemoteObject(handle: UInt32(handle))) { }.cookie + } + } + var cookies = Set() + for await cookie in group { cookies.insert(cookie) } + #expect(cookies.count == 100) + } + #expect(registry.count == 100) + } + + // MARK: - Delivery + + /// A death fires the handler registered for that cookie, and only that one. + @Test func deliversToTheMatchingCookie() { + let registry = DeathRegistry() + let firedFirst = Mutex(false) + let firedSecond = Mutex(false) + + let first = registry.register(object: RemoteObject(handle: 1)) { + firedFirst.withLock { $0 = true } + } + _ = registry.register(object: RemoteObject(handle: 2)) { + firedSecond.withLock { $0 = true } + } + + registry.notification(for: first.cookie)?.handler() + + #expect(firedFirst.withLock { $0 }) + #expect(firedSecond.withLock { $0 } == false) + } + + /// A death arriving for a cookie already retired must not crash the loop; + /// it still has to be acknowledged or the driver keeps the recipient. + @Test func anUnknownCookieHasNoHandler() { + let registry = DeathRegistry() + #expect(registry.notification(for: 999) == nil) + } + + /// Cancelling does not retire the record — a death may already be in + /// flight, and BR_CLEAR_DEATH_NOTIFICATION_DONE is what actually retires + /// it. Dropping it at cancel time would leave that arrival unattributable + /// and the driver waiting on an acknowledgement that never came. + @Test func removalIsDrivenByTheConfirmation() { + let registry = DeathRegistry() + let notification = registry.register(object: RemoteObject(handle: 1)) { } + + #expect(registry.count == 1) + let removed = registry.remove(cookie: notification.cookie) + #expect(removed === notification) + #expect(registry.count == 0) + #expect(registry.remove(cookie: notification.cookie) == nil) + } + + // MARK: - Ping + + /// IBinder::PING_TRANSACTION, the four characters `_PNG` packed into an + /// integer. Transaction codes are interface-defined, so the universal ones + /// sit far outside the range an interface would choose. + @Test func pingTransactionIsPackedCharacters() { + #expect(BinderConnection.pingTransaction == 0x5F50_4E47) + + let characters = [UInt8(ascii: "_"), UInt8(ascii: "P"), + UInt8(ascii: "N"), UInt8(ascii: "G")] + let packed = characters.reduce(UInt32(0)) { ($0 << 8) | UInt32($1) } + #expect(BinderConnection.pingTransaction == packed) + } +} diff --git a/Tests/BinderTests/DriverStreamTests.swift b/Tests/BinderTests/DriverStreamTests.swift new file mode 100644 index 0000000..513cda5 --- /dev/null +++ b/Tests/BinderTests/DriverStreamTests.swift @@ -0,0 +1,176 @@ +// +// DriverStreamTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +@Suite +struct DriverStreamTests { + + // MARK: - Payload sizes + + /// The size embedded in each command code must equal the structure it + /// names. This is what lets the stream be walked without a hand-written + /// table, so it is asserted against `MemoryLayout` rather than assumed. + @Test func returnCommandSizesMatchTheirStructures() { + #expect(DriverReturnProtocol.reply.payloadSize + == MemoryLayout.size) + #expect(DriverReturnProtocol.transaction.payloadSize + == MemoryLayout.size) + #expect(DriverReturnProtocol.incRefs.payloadSize + == MemoryLayout.size) + #expect(DriverReturnProtocol.deadBinder.payloadSize + == MemoryLayout.size) + #expect(DriverReturnProtocol.error.payloadSize == MemoryLayout.size) + } + + @Test func commandsWithoutPayloadsReportZero() { + #expect(DriverReturnProtocol.noop.payloadSize == 0) + #expect(DriverReturnProtocol.transactionComplete.payloadSize == 0) + #expect(DriverReturnProtocol.deadReply.payloadSize == 0) + #expect(DriverReturnProtocol.failedReply.payloadSize == 0) + #expect(DriverCommandProtocol.enterLooper.payloadSize == 0) + } + + @Test func driverCommandSizesMatchTheirStructures() { + #expect(DriverCommandProtocol.transaction.payloadSize + == MemoryLayout.size) + #expect(DriverCommandProtocol.freeBuffer.payloadSize + == MemoryLayout.size) + #expect(DriverCommandProtocol.acquire.payloadSize == MemoryLayout.size) + } + + // MARK: - Writing commands + + /// A command is a bare 32-bit code with no framing around it. + @Test func appendsCommandCodeWithNoFraming() { + var stream = CommandStream() + stream.append(.enterLooper) + #expect(stream.count == 4) + #expect(stream.bytes == withUnsafeBytes(of: BC_ENTER_LOOPER.rawValue) { Array($0) }) + } + + /// The payload follows the code immediately: no padding, no length. + @Test func appendsPayloadImmediatelyAfterTheCode() { + var stream = CommandStream() + let pointer = binder_uintptr_t(0xDEADBEEF) + stream.append(.freeBuffer, payload: pointer) + + #expect(stream.count == 4 + MemoryLayout.size) + let code = stream.bytes.prefix(4).withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } + #expect(code == BC_FREE_BUFFER.rawValue) + let value = Array(stream.bytes.dropFirst(4)) + .withUnsafeBytes { $0.loadUnaligned(as: binder_uintptr_t.self) } + #expect(value == pointer) + } + + @Test func packsSeveralCommandsBackToBack() { + var stream = CommandStream() + stream.append(.enterLooper) + stream.append(.freeBuffer, payload: binder_uintptr_t(1)) + stream.append(.exitLooper) + #expect(stream.count == 4 + (4 + 8) + 4) + } + + // MARK: - Reading returns + + /// Build a driver-style stream by hand, the way the kernel would. + private func encoded(_ commands: [(UInt32, [UInt8])]) -> [UInt8] { + var bytes = [UInt8]() + for (code, payload) in commands { + withUnsafeBytes(of: code) { bytes.append(contentsOf: $0) } + bytes.append(contentsOf: payload) + } + return bytes + } + + /// The realistic shape: a reply is preceded by bookkeeping commands that + /// have nothing to do with it. + @Test func walksNoopAndCompleteBeforeAReply() throws { + let reply = [UInt8](repeating: 0xAB, count: MemoryLayout.size) + var stream = ReturnStream(bytes: encoded([ + (BR_NOOP.rawValue, []), + (BR_TRANSACTION_COMPLETE.rawValue, []), + (BR_REPLY.rawValue, reply), + ])) + + let first = try stream.next() + #expect(first?.command == .noop) + #expect(first?.payload.isEmpty == true) + + let second = try stream.next() + #expect(second?.command == .transactionComplete) + + let third = try stream.next() + #expect(third?.command == .reply) + #expect(third?.payload.count == MemoryLayout.size) + + #expect(stream.isAtEnd) + #expect(try stream.next() == nil) + } + + @Test func decodesAPayloadAsItsStructure() throws { + var data = binder_transaction_data() + data.code = 42 + data.flags = TransactionFlags.acceptFds.rawValue + let payload = withUnsafeBytes(of: data) { Array($0) } + + var stream = ReturnStream(bytes: encoded([(BR_REPLY.rawValue, payload)])) + let element = try #require(try stream.next()) + let decoded = try #require(element.decode(as: binder_transaction_data.self)) + + #expect(decoded.code == 42) + #expect(decoded.flags == TransactionFlags.acceptFds.rawValue) + } + + @Test func decodeRejectsAMismatchedSize() throws { + var stream = ReturnStream(bytes: encoded([(BR_ERROR.rawValue, [1, 0, 0, 0])])) + let element = try #require(try stream.next()) + #expect(element.decode(as: Int32.self) == 1) + #expect(element.decode(as: binder_transaction_data.self) == nil) + } + + @Test func reportsTruncationPartWayThroughAPayload() { + // BR_REPLY promises 64 bytes; supply 8. + var stream = ReturnStream(bytes: encoded([ + (BR_REPLY.rawValue, [UInt8](repeating: 0, count: 8)), + ])) + #expect(throws: ReturnStreamError.truncated) { try stream.next() } + } + + @Test func reportsTruncationPartWayThroughACode() { + var stream = ReturnStream(bytes: [0x01, 0x02]) + #expect(throws: ReturnStreamError.truncated) { try stream.next() } + } + + /// An unrecognised command has an unknown payload length, so the rest of + /// the stream cannot be walked. Failing beats skipping and producing + /// plausible nonsense from then on. + @Test func refusesToGuessPastAnUnknownCommand() throws { + var stream = ReturnStream(bytes: encoded([ + (BR_NOOP.rawValue, []), + (0xDEAD_BEEF, []), + ])) + #expect(try stream.next()?.command == .noop) + #expect(throws: ReturnStreamError.unknownCommand(0xDEAD_BEEF)) { try stream.next() } + } + + @Test func emptyStreamYieldsNothing() throws { + var stream = ReturnStream(bytes: []) + #expect(stream.isAtEnd) + #expect(try stream.next() == nil) + } + + /// BR_TRANSACTION and BR_TRANSACTION_SEC_CTX share an ioctl number and are + /// told apart only by the size of the structure they carry. If those ever + /// collided, a security-context transaction would decode as a plain one. + @Test func transactionAndSecCtxAreDistinctCodes() { + #expect(DriverReturnProtocol.transaction != DriverReturnProtocol.transactionSecCtx) + #expect(DriverReturnProtocol.transaction.payloadSize + != DriverReturnProtocol.transactionSecCtx.payloadSize) + } +} diff --git a/Tests/BinderTests/FileDescriptorObjectTests.swift b/Tests/BinderTests/FileDescriptorObjectTests.swift new file mode 100644 index 0000000..ce58c09 --- /dev/null +++ b/Tests/BinderTests/FileDescriptorObjectTests.swift @@ -0,0 +1,86 @@ +// +// FileDescriptorObjectTests.swift +// Binder +// + +import CBinder +import SystemPackage +import Testing + +@testable import Binder + +@Suite +struct FileDescriptorObjectTests { + + /// A binder_fd_object shares the 24-byte envelope of a binder object, with + /// the descriptor in the union at offset 8. If that shifted, the driver + /// would translate the wrong word. + @Test func fdObjectSharesTheObjectEnvelope() { + #expect(MemoryLayout.size + == MemoryLayout.size) + } + + /// A descriptor is written at a declared offset, like any object, so the + /// driver knows to dup it rather than pass the number through as data. + @Test func writesADescriptorAsAnObject() { + var parcel = Parcel() + parcel.append(fileDescriptor: FileDescriptor(rawValue: 7)) + + #expect(parcel.objectOffsets == [0]) + #expect(parcel.count == MemoryLayout.size) + + let object = parcel.bytes.withUnsafeBytes { + $0.loadUnaligned(as: binder_fd_object.self) + } + #expect(object.hdr.type == BinderType.fileDescriptor.rawValue) + #expect(object.fd == 7) + } + + @Test func roundTripsADescriptor() throws { + var parcel = Parcel() + parcel.append(fileDescriptor: FileDescriptor(rawValue: 42)) + + var reader = parcel.reader() + let fd = try reader.readFileDescriptor() + #expect(fd?.rawValue == 42) + #expect(reader.isAtEnd) + } + + /// A descriptor is not a binder object reference, so enumerating a parcel's + /// remote objects must not turn it up — acquiring it would be meaningless + /// and freeing the reply would try to drop a reference that was never taken. + @Test func fileDescriptorsAreNotRemoteObjects() { + var parcel = Parcel() + parcel.append(fileDescriptor: FileDescriptor(rawValue: 3)) + #expect(parcel.objects.isEmpty) + } + + /// Reading an object as a descriptor, or the reverse, returns nil rather + /// than a wrong answer — the header type is checked, not assumed. + @Test func readFileDescriptorRejectsANonDescriptorObject() throws { + var parcel = Parcel() + parcel.append(RemoteObject(handle: 9)) + + var reader = parcel.reader() + #expect(try reader.readFileDescriptor() == nil) + } + + @Test func readObjectRejectsADescriptor() throws { + var parcel = Parcel() + parcel.append(fileDescriptor: FileDescriptor(rawValue: 4)) + + var reader = parcel.reader() + #expect(try reader.readObject() == nil) + } + + /// A negative descriptor round-trips unchanged: the union stores the raw + /// bit pattern, and -1 is a legitimate "no descriptor" sentinel a sender + /// may use. + @Test func preservesTheDescriptorBitPattern() throws { + var parcel = Parcel() + parcel.append(fileDescriptor: FileDescriptor(rawValue: -1)) + + var reader = parcel.reader() + #expect(try reader.readFileDescriptor()?.rawValue == -1) + } +} diff --git a/Tests/BinderTests/IntegrationTests.swift b/Tests/BinderTests/IntegrationTests.swift new file mode 100644 index 0000000..d169804 --- /dev/null +++ b/Tests/BinderTests/IntegrationTests.swift @@ -0,0 +1,269 @@ +// +// IntegrationTests.swift +// Binder +// + +import CBinder +import SystemPackage +import Testing + +@testable import Binder + +/// Full request→reply round trips through the real encode and decode paths. +/// +/// A binder parcel is host-order shared memory, not a network format, so a +/// request one side writes and the other reads is byte-for-byte what would +/// cross the kernel — the driver only copies those bytes and translates object +/// handles and offsets. That makes a same-process round trip a faithful test of +/// everything except the handle translation itself: the interface token, the +/// argument order, the reply's exception header, the object and buffer +/// encodings, and the way they compose. +/// +/// The piecewise unit tests cover each of those alone. These cover them +/// together, which is where the integration bugs found while building this +/// library actually lived — a field written in the wrong order, an offset not +/// recorded, a reference dropped before it was read. +@Suite +struct IntegrationTests { + + // MARK: - Peers, written the way a real one would decode and reply + + /// Stands in for `servicemanager`: decodes a lookup and answers with an + /// object or a null binder, exactly as the real one does. + private func serviceManagerReply( + to request: Parcel, + registry: [String: UInt32] + ) throws -> Parcel { + var reader = request.reader() + let descriptor = try reader.readInterfaceToken() + #expect(descriptor == ServiceManager.descriptor) + let name = try #require(try reader.readString()) + + var reply = Parcel() + reply.append(Int32(0)) // exception: none + if let handle = registry[name] { + reply.append(RemoteObject(handle: handle)) + } else { + // A null binder: type is a binder, handle zero. + var null = flat_binder_object() + null.hdr = binder_object_header(type: .binder) + reply.append(bitPattern: null) + } + return reply + } + + // MARK: - Service manager lookup + + @Test func looksUpAServiceAndDecodesItsHandle() throws { + // What lookUp(_:using:header:) builds for getService. + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append("activity") + + let reply = try serviceManagerReply(to: request, registry: ["activity": 42]) + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + #expect(try reader.readObject() == RemoteObject(handle: 42)) + } + + @Test func absentServiceComesBackAsNull() throws { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append("nonesuch") + + let reply = try serviceManagerReply(to: request, registry: [:]) + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + // The null binder decodes as absence, not as handle zero. + #expect(try reader.readObject() == nil) + } + + // MARK: - An AIDL method with mixed arguments + + /// The shape of a real call: interface token, then arguments in declaration + /// order, answered with an exception header and the return values. Every + /// field is checked on the far side, so a mis-ordered write or a wrong + /// width fails here rather than silently shifting everything after it. + @Test func roundTripsAnAIDLMethod() throws { + // Client encodes: setThing(int id, String name, boolean enabled, long ts) + var request = Parcel() + request.appendInterfaceToken("com.example.IThing") + request.append(Int32(7)) + request.append("widget") + request.append(true) + request.append(Int64(1_700_000_000_000)) + + // Service decodes in the same order. + var reader = request.reader() + #expect(try reader.readInterfaceToken() == "com.example.IThing") + let id = try reader.readInt32() + let name = try #require(try reader.readString()) + let enabled = try reader.readBool() + let timestamp = try reader.readInt64() + #expect(id == 7) + #expect(name == "widget") + #expect(enabled == true) + #expect(timestamp == 1_700_000_000_000) + #expect(reader.isAtEnd) + + // Service replies: no exception, then (String result, int code). + var reply = Parcel() + reply.append(Int32(0)) + reply.append("ok:\(name)") + reply.append(id * 2) + + var replyReader = reply.reader() + #expect(try replyReader.readInt32() == 0) + #expect(try replyReader.readString() == "ok:widget") + #expect(try replyReader.readInt32() == 14) + #expect(replyReader.isAtEnd) + } + + /// A thrown service-specific error: the reply's exception header is + /// non-zero and the client stops before reading a return value that is not + /// there. + @Test func remoteExceptionIsSeenBeforeDecodingAResult() throws { + var reply = Parcel() + reply.append(Int32(-8)) // EX_SERVICE_SPECIFIC + + var reader = reply.reader() + let exception = try reader.readInt32() + #expect(exception != 0) + // A client stops here; there is no return value to read. + } + + // MARK: - A hosted object, through the real handler path + + /// Register a real `LocalObject`, build the request a client would send, + /// and drive it through the object's own handler — the same closure the + /// serving loop invokes — then decode the reply as the client would. + @Test func dispatchesToAHostedObjectsHandler() throws { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "com.example.IEcho") { transaction in + // Only a trusted caller, checked on driver-supplied identity. + guard transaction.senderUserID == 1000 else { + var denied = Parcel() + denied.append(Int32(-1)) // EX_SECURITY + return denied + } + var body = transaction.request.reader() + _ = try? body.readInterfaceToken(header: .system) + let message = (try? body.readString()) ?? nil + + var reply = Parcel() + reply.append(Int32(0)) + reply.append("echo:\(message ?? "")") + return reply + } + + // Client side. + var request = Parcel() + request.appendInterfaceToken("com.example.IEcho") + request.append("hello") + + // The loop builds this from binder_transaction_data; here it is built + // directly with the driver-supplied identity a real transaction carries. + let transaction = LocalObject.Transaction( + code: 1, + request: request, + flags: .acceptFds, + senderProcessID: 4321, + senderUserID: 1000 + ) + let reply = try #require(try object.handler(transaction)) + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + #expect(try reader.readString() == "echo:hello") + } + + @Test func hostedObjectRejectsAnUntrustedCaller() throws { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "com.example.IEcho") { transaction in + guard transaction.senderUserID == 1000 else { + var denied = Parcel() + denied.append(Int32(-1)) + return denied + } + return Parcel() + } + + let transaction = LocalObject.Transaction( + code: 1, + request: Parcel(), + flags: [], + senderProcessID: 9, + senderUserID: 2000 // not the trusted uid + ) + let reply = try #require(try object.handler(transaction)) + + var reader = reply.reader() + #expect(try reader.readInt32() == -1) + } + + // MARK: - A HIDL-shaped call carrying an out-of-line buffer + + /// A request with an inline header and a vector passed as a buffer, decoded + /// the way a received parcel presents it: the payload captured out of band, + /// read back through the buffer's offset. + @Test func roundTripsAnOutOfLineBuffer() throws { + var request = Parcel() + request.appendInterfaceToken("android.hardware.foo@1.0::IFoo") + request.append(Int32(3)) // element count + request.append(buffer: [10, 20, 30]) // the vector's bytes + + #expect(request.hasBuffers) + #expect(request.buffersSize == 8) // 3 bytes, aligned to 8 + + // Reconstruct the parcel as copyReply hands it over: inline bytes and + // offsets unchanged, each buffer's payload captured separately. + let received = Parcel( + bytes: request.bytes, + objectOffsets: request.objectOffsets, + scatterGatherBuffers: request.scatterGatherBuffers + ) + + var reader = received.reader() + #expect(try reader.readInterfaceToken() == "android.hardware.foo@1.0::IFoo") + #expect(try reader.readInt32() == 3) + #expect(try reader.readBuffer(from: received) == [10, 20, 30]) + #expect(reader.isAtEnd) + } + + // MARK: - Objects and descriptors crossing together + + /// A reply that returns both a binder and a file descriptor, at their own + /// offsets, each decoded by the right reader and not mistaken for the other. + @Test func roundTripsAnObjectAndADescriptorTogether() throws { + var reply = Parcel() + reply.append(Int32(0)) + reply.append(RemoteObject(handle: 5)) + reply.append(fileDescriptor: FileDescriptor(rawValue: 9)) + + #expect(reply.objectOffsets.count == 2) + // Only the binder is a reference to be acquired; the fd is not. + #expect(reply.objects == [RemoteObject(handle: 5)]) + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + #expect(try reader.readObject() == RemoteObject(handle: 5)) + #expect(try reader.readFileDescriptor()?.rawValue == 9) + #expect(reader.isAtEnd) + } + + // MARK: - Truncated reply + + /// A reply shorter than the interface promises fails at the first read past + /// the end, rather than returning a plausible value — the case a version + /// skew between the two sides produces. + @Test func aShortReplyFailsRatherThanInventingData() throws { + var reply = Parcel() + reply.append(Int32(0)) // exception only; the promised string is missing + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + #expect(throws: ParcelError.endOfParcel) { try reader.readString() } + } +} diff --git a/Tests/BinderTests/LooperTests.swift b/Tests/BinderTests/LooperTests.swift new file mode 100644 index 0000000..35013a2 --- /dev/null +++ b/Tests/BinderTests/LooperTests.swift @@ -0,0 +1,214 @@ +// +// LooperTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +/// The registry and the local-object encoding, which need no driver. +/// +/// The serving loop's ioctls cannot run here, but what goes wrong in a loop is +/// almost never the ioctl — it is answering the wrong command, or failing to +/// answer one the driver is waiting on. Those decisions live in +/// `action(for:)`, which needs a connection, so the parts reachable without one +/// are covered here and the rest is left to a device. +@Suite +struct LocalObjectTests { + + // MARK: - Registry + + @Test func registersAndFindsAnObject() { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "com.example.IThing") { _ in nil } + + #expect(registry.count == 1) + #expect(registry.object(for: object.id) === object) + #expect(object.interfaceDescriptor == "com.example.IThing") + } + + /// Zero is a null binder on the wire, so no object may be given that id. + @Test func idsStartAtOne() { + let registry = LocalObjectRegistry() + let first = registry.register(interfaceDescriptor: "a") { _ in nil } + #expect(first.id == 1) + } + + /// The driver keys its node on this value, so two live objects sharing one + /// would send every transaction for either to whichever was registered + /// second. + @Test func idsAreUnique() { + let registry = LocalObjectRegistry() + let ids = (0 ..< 50).map { _ in + registry.register(interfaceDescriptor: "a") { _ in nil }.id + } + #expect(Set(ids).count == ids.count) + } + + @Test func withdrawnObjectsAreNotFound() { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "a") { _ in nil } + registry.remove(object) + + #expect(registry.count == 0) + #expect(registry.object(for: object.id) == nil) + } + + /// Ids are not reused after a withdrawal. Reusing one would let a + /// transaction already in flight for the old object arrive at a new one. + @Test func idsAreNotReusedAfterWithdrawal() { + let registry = LocalObjectRegistry() + let first = registry.register(interfaceDescriptor: "a") { _ in nil } + registry.remove(first) + let second = registry.register(interfaceDescriptor: "a") { _ in nil } + + #expect(second.id != first.id) + } + + /// The registry is reached from every serving thread at once. + @Test func registrationIsSafeUnderConcurrency() async { + let registry = LocalObjectRegistry() + + await withTaskGroup(of: UInt64.self) { group in + for _ in 0 ..< 100 { + group.addTask { + registry.register(interfaceDescriptor: "a") { _ in nil }.id + } + } + var ids = Set() + for await id in group { ids.insert(id) } + #expect(ids.count == 100) + } + #expect(registry.count == 100) + } + + // MARK: - Encoding + + /// BINDER_TYPE_BINDER is what tells the driver the object lives here: it + /// creates a node and hands the receiver a handle pointing back. Writing + /// BINDER_TYPE_HANDLE instead would claim to be forwarding a reference to + /// somewhere else. + @Test func localObjectsAreWrittenAsNodesNotHandles() { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "a") { _ in nil } + + var parcel = Parcel() + parcel.append(object) + + #expect(parcel.objectOffsets == [0]) + #expect(parcel.count == MemoryLayout.size) + + let flat = parcel.bytes.withUnsafeBytes { + $0.loadUnaligned(as: flat_binder_object.self) + } + #expect(flat.hdr.type == BinderType.binder.rawValue) + #expect(flat.binder == binder_uintptr_t(object.id)) + #expect(flat.cookie == binder_uintptr_t(object.id)) + } + + /// A hosted object is not a remote reference, so enumerating a parcel's + /// remote objects must not turn one up — acquiring it would be meaningless. + @Test func localObjectsAreNotRemoteObjects() { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "a") { _ in nil } + + var parcel = Parcel() + parcel.append(object) + + #expect(parcel.objects.isEmpty) + } + + // MARK: - Handlers + + @Test func handlerReceivesTheTransaction() throws { + let registry = LocalObjectRegistry() + var request = Parcel() + request.append(Int32(7)) + + let object = registry.register(interfaceDescriptor: "a") { transaction in + #expect(transaction.code == 3) + #expect(transaction.senderUserID == 1000) + var reply = Parcel() + reply.append(Int32(0)) + return reply + } + + let transaction = LocalObject.Transaction( + code: 3, + request: request, + flags: .acceptFds, + senderProcessID: 42, + senderUserID: 1000 + ) + let reply = try #require(try object.handler(transaction)) + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + } + + /// The one thing the driver fills in that a caller cannot forge. + @Test func senderIdentityComesFromTheDriver() { + var data = binder_transaction_data() + data.sender_pid = 1234 + data.sender_euid = 10056 + + let transaction = LocalObject.Transaction( + code: 1, + request: Parcel(), + flags: [], + senderProcessID: data.sender_pid, + senderUserID: data.sender_euid + ) + #expect(transaction.senderProcessID == 1234) + #expect(transaction.senderUserID == 10056) + } + + // MARK: - Acknowledgement commands + + /// The driver blocks a node's reference transition until the matching + /// *_DONE arrives, so these have to carry a binder_ptr_cookie and be sent. + @Test(arguments: [DriverCommandProtocol.incRefsDone, .acquireDone]) + func acknowledgementsCarryAPointerAndCookie(_ command: DriverCommandProtocol) { + #expect(command.payloadSize == MemoryLayout.size) + + var cookie = binder_ptr_cookie() + cookie.ptr = 0x1111 + cookie.cookie = 0x2222 + + var stream = CommandStream() + stream.append(command, payload: cookie) + #expect(stream.count == 4 + MemoryLayout.size) + + let decoded = Array(stream.bytes.dropFirst(4)) + .withUnsafeBytes { $0.loadUnaligned(as: binder_ptr_cookie.self) } + #expect(decoded.ptr == 0x1111) + #expect(decoded.cookie == 0x2222) + } + + /// BR_INCREFS is answered by BC_INCREFS_DONE and BR_ACQUIRE by + /// BC_ACQUIRE_DONE. Crossing them leaves the driver waiting on a transition + /// that never completes. + @Test func acknowledgementsPairWithTheirRequests() { + #expect(DriverReturnProtocol.incRefs.payloadSize + == DriverCommandProtocol.incRefsDone.payloadSize) + #expect(DriverReturnProtocol.acquire.payloadSize + == DriverCommandProtocol.acquireDone.payloadSize) + #expect(DriverCommandProtocol.incRefsDone != DriverCommandProtocol.acquireDone) + } + + // MARK: - Looper commands + + @Test func looperCommandsCarryNoPayload() { + #expect(DriverCommandProtocol.enterLooper.payloadSize == 0) + #expect(DriverCommandProtocol.exitLooper.payloadSize == 0) + #expect(DriverCommandProtocol.registerLooper.payloadSize == 0) + } + + /// A reply carries a full transaction structure, same as a request. + @Test func replyCarriesATransactionStructure() { + #expect(DriverCommandProtocol.reply.payloadSize + == MemoryLayout.size) + } +} diff --git a/Tests/BinderTests/MappedBufferTests.swift b/Tests/BinderTests/MappedBufferTests.swift new file mode 100644 index 0000000..3fa457e --- /dev/null +++ b/Tests/BinderTests/MappedBufferTests.swift @@ -0,0 +1,38 @@ +// +// MappedBufferTests.swift +// Binder +// + +import Testing + +@testable import Binder + +@Suite +struct MappedBufferTests { + + /// One megabyte less two pages, matching libbinder's BINDER_VM_SIZE. + /// + /// Worth asserting rather than trusting: services on the other end are + /// provisioned assuming this figure, and the two-page subtraction is the + /// part that looks like it could be dropped without consequence. + @Test func defaultSizeIsOneMegabyteLessTwoPages() { + let pageSize = MappedBuffer.defaultSize + #expect(pageSize < 1024 * 1024) + #expect(pageSize >= (1024 * 1024) - (64 * 1024 * 2)) + #expect((1024 * 1024) - pageSize == 2 * ((1024 * 1024) - pageSize) / 2) + } + + /// The mapping is a whole number of pages below a megabyte. + @Test func defaultSizeIsPageAligned() { + let overhead = (1024 * 1024) - MappedBuffer.defaultSize + #expect(overhead % 2 == 0) + // Each of the two pages is a power of two. + let page = overhead / 2 + #expect(page > 0) + #expect(page & (page - 1) == 0) + } + + @Test func connectionExposesTheSameDefault() { + #expect(BinderConnection.defaultBufferSize == MappedBuffer.defaultSize) + } +} diff --git a/Tests/BinderTests/NestedBufferTests.swift b/Tests/BinderTests/NestedBufferTests.swift new file mode 100644 index 0000000..00dd6a7 --- /dev/null +++ b/Tests/BinderTests/NestedBufferTests.swift @@ -0,0 +1,99 @@ +// +// NestedBufferTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +@Suite +struct NestedBufferTests { + + /// A top-level buffer returns its object index, which a child names as its + /// parent. The index is into the offsets array, counting every object, not + /// only buffers. + @Test func topLevelBufferReturnsItsObjectIndex() { + var parcel = Parcel() + parcel.append(RemoteObject(handle: 1)) // object 0 + let outer = parcel.append(buffer: [0, 0, 0, 0, 0, 0, 0, 0]) // object 1 + #expect(outer == 1) + } + + /// A child buffer carries the parent flag, the parent index, and the offset + /// within the parent where the driver writes its address. Getting any of + /// the three wrong sends the driver to the wrong place, or has it treat a + /// nested buffer as top-level and never relocate it into the parent. + @Test func nestedBufferCarriesParentLinkage() { + var parcel = Parcel() + // A parent whose payload has an 8-byte hole at offset 0 for the child's + // pointer. + let parent = parcel.append(buffer: [UInt8](repeating: 0, count: 8)) + parcel.append(buffer: [1, 2, 3], parent: parent, parentOffset: 0) + + // Decode the child descriptor, the second buffer object. + let childOffset = parcel.objectOffsets.last! + let child = parcel.bytes[childOffset...] + .withUnsafeBytes { $0.loadUnaligned(as: binder_buffer_object.self) } + + #expect(child.hdr.type == BinderType.pointer.rawValue) + #expect((child.flags & 0x01) == 0x01) // BINDER_BUFFER_FLAG_HAS_PARENT + #expect(child.parent == binder_size_t(parent)) + #expect(child.parent_offset == 0) + #expect(child.length == 3) + } + + /// A top-level buffer has no parent, so the flag stays clear — otherwise the + /// driver would look for a parent that is not there. + @Test func topLevelBufferHasNoParentFlag() { + var parcel = Parcel() + parcel.append(buffer: [1, 2, 3]) + let object = parcel.bytes.withUnsafeBytes { + $0.loadUnaligned(as: binder_buffer_object.self) + } + #expect((object.flags & 0x01) == 0) + #expect(object.parent == 0) + #expect(object.parent_offset == 0) + } + + /// Both buffers count toward the parcel's object offsets and its reserved + /// size, parent and child alike. + @Test func bothBuffersAreTrackedAndReserved() { + var parcel = Parcel() + let parent = parcel.append(buffer: [UInt8](repeating: 0, count: 8)) // -> 8 + parcel.append(buffer: [1, 2, 3], parent: parent, parentOffset: 0) // -> 8 + + #expect(parcel.objectOffsets.count == 2) + #expect(parcel.buffersSize == 16) + #expect(parcel.hasBuffers) + } + + /// The shape of a hidl_vec with two elements: an outer buffer + /// of two 16-byte string structs, each with a child buffer holding its + /// characters, linked at the offset of that struct's pointer field. + @Test func laysOutAVectorOfStrings() { + // Two hidl_string structs, 16 bytes each: a pointer (8) and a size (8), + // pointer at the start of each. Placeholders for the pointers. + var parcel = Parcel() + let vector = parcel.append(buffer: [UInt8](repeating: 0, count: 32)) + + // First element's characters, pointer at offset 0 of the vector. + parcel.append(buffer: Array("ab".utf8), parent: vector, parentOffset: 0) + // Second element's characters, pointer at offset 16. + parcel.append(buffer: Array("cde".utf8), parent: vector, parentOffset: 16) + + #expect(parcel.objectOffsets.count == 3) + // 32 (aligned) + 8 ("ab"→8) + 8 ("cde"→8). + #expect(parcel.buffersSize == 48) + + // Both children name the vector as parent, at their own offsets. + let children = parcel.objectOffsets.dropFirst().map { offset in + parcel.bytes[offset...].withUnsafeBytes { + $0.loadUnaligned(as: binder_buffer_object.self) + } + } + #expect(children.allSatisfy { $0.parent == binder_size_t(vector) }) + #expect(children.map(\.parent_offset) == [0, 16]) + } +} diff --git a/Tests/BinderTests/ParcelTests.swift b/Tests/BinderTests/ParcelTests.swift new file mode 100644 index 0000000..dd1b6d5 --- /dev/null +++ b/Tests/BinderTests/ParcelTests.swift @@ -0,0 +1,235 @@ +// +// ParcelTests.swift +// Binder +// + +import Testing + +@testable import Binder + +@Suite +struct ParcelTests { + + // MARK: - Layout + + @Test(arguments: [(0, 0), (1, 4), (2, 4), (3, 4), (4, 4), (5, 8), (7, 8), (8, 8), (9, 12)]) + func padsToFourByteBoundary(_ length: Int, _ expected: Int) { + #expect(Parcel.paddedSize(length) == expected) + } + + // MARK: - Scalars + + /// Expected bytes are written out rather than produced by the encoder, so + /// the test fails if the encoding changes rather than agreeing with it. + @Test func appendsInt32AsFourBytes() { + var parcel = Parcel() + parcel.append(Int32(1)) + #expect(parcel.bytes == [0x01, 0x00, 0x00, 0x00]) + #expect(parcel.count == 4) + } + + @Test func appendsInt64AsEightBytes() { + var parcel = Parcel() + parcel.append(Int64(1)) + #expect(parcel.bytes == [0x01, 0, 0, 0, 0, 0, 0, 0]) + } + + /// Int64 sits at 4-byte alignment, never 8. If this ever pads to 8 the + /// parcel silently desynchronises against every Android reader. + @Test func doesNotAlignInt64ToEightBytes() throws { + var parcel = Parcel() + parcel.append(Int32(0x11111111)) + parcel.append(Int64(0x2222222233333333)) + #expect(parcel.count == 12) + + var reader = parcel.reader() + #expect(try reader.readInt32() == 0x11111111) + #expect(try reader.readInt64() == 0x2222222233333333) + #expect(reader.isAtEnd) + } + + /// Bool, Int8 and UTF-16 code units are all widened to Int32 on the wire. + @Test func widensSmallScalarsToInt32() { + var parcel = Parcel() + parcel.append(true) + parcel.append(byte: -1) + parcel.append(character: 0x41) + #expect(parcel.count == 12) + #expect(Array(parcel.bytes.prefix(4)) == [0x01, 0x00, 0x00, 0x00]) + #expect(Array(parcel.bytes.dropFirst(4).prefix(4)) == [0xFF, 0xFF, 0xFF, 0xFF]) + } + + @Test func roundTripsScalars() throws { + var parcel = Parcel() + parcel.append(Int32.min) + parcel.append(Int64.max) + parcel.append(false) + parcel.append(byte: Int8.min) + parcel.append(character: 0xFFFF) + + var reader = parcel.reader() + #expect(try reader.readInt32() == Int32.min) + #expect(try reader.readInt64() == Int64.max) + #expect(try reader.readBool() == false) + #expect(try reader.readByte() == Int8.min) + #expect(try reader.readCharacter() == 0xFFFF) + #expect(reader.isAtEnd) + } + + /// A sender is not obliged to normalise a boolean to 1. + @Test func readsAnyNonZeroAsTrue() throws { + var parcel = Parcel() + parcel.append(Int32(42)) + var reader = parcel.reader() + #expect(try reader.readBool() == true) + } + + // MARK: - Strings + + /// Length in UTF-16 code units, then the units, then a NUL terminator that + /// is not counted in the length, then padding. + @Test func encodesString16WithTerminatorOutsideTheLength() { + var parcel = Parcel() + parcel.append("Hi") + #expect(parcel.bytes == [ + 0x02, 0x00, 0x00, 0x00, // length: 2 code units + 0x48, 0x00, // 'H' + 0x69, 0x00, // 'i' + 0x00, 0x00, // NUL terminator + 0x00, 0x00, // padding to 12 + ]) + } + + @Test func encodesEmptyStringAsLengthZeroPlusTerminator() { + var parcel = Parcel() + parcel.append("") + #expect(parcel.bytes == [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + } + + /// Null and empty are different values on the wire and AIDL cares. + @Test func distinguishesNullStringFromEmptyString() throws { + var parcel = Parcel() + parcel.appendNullString() + parcel.append("") + + #expect(Array(parcel.bytes.prefix(4)) == [0xFF, 0xFF, 0xFF, 0xFF]) + + var reader = parcel.reader() + #expect(try reader.readString() == nil) + #expect(try reader.readString() == "") + } + + @Test(arguments: ["", "a", "ab", "abc", "hello world", "café", "日本語", "🙂"]) + func roundTripsStrings(_ value: String) throws { + var parcel = Parcel() + parcel.append(value) + var reader = parcel.reader() + #expect(try reader.readString() == value) + #expect(reader.isAtEnd) + } + + /// An emoji is one Character but two UTF-16 code units, and the length + /// prefix counts code units. + @Test func countsSurrogatePairsAsTwoCodeUnits() throws { + var parcel = Parcel() + parcel.append("🙂") + var reader = parcel.reader() + #expect(try reader.readInt32() == 2) + } + + @Test func rejectsUnterminatedString() { + // Length 1, then a code unit, then a non-zero where the terminator + // should be. + let parcel = Parcel(bytes: [0x01, 0, 0, 0, 0x41, 0x00, 0x42, 0x00]) + var reader = parcel.reader() + #expect(throws: ParcelError.unterminatedString) { try reader.readString() } + } + + // MARK: - Byte arrays + + /// Byte arrays are not widened; only the length prefix is. + @Test func encodesByteArrayWithoutWidening() { + var parcel = Parcel() + parcel.append(bytes: [0xDE, 0xAD, 0xBE]) + #expect(parcel.bytes == [ + 0x03, 0x00, 0x00, 0x00, + 0xDE, 0xAD, 0xBE, + 0x00, // padding + ]) + } + + @Test(arguments: [[], [0x01], [0x01, 0x02], [0x01, 0x02, 0x03], [0x01, 0x02, 0x03, 0x04]]) + func roundTripsByteArrays(_ value: [UInt8]) throws { + var parcel = Parcel() + parcel.append(bytes: value) + var reader = parcel.reader() + #expect(try reader.readBytes() == value) + #expect(reader.isAtEnd) + } + + @Test func distinguishesNullByteArrayFromEmpty() throws { + var parcel = Parcel() + parcel.appendNullBytes() + parcel.append(bytes: []) + + var reader = parcel.reader() + #expect(try reader.readBytes() == nil) + #expect(try reader.readBytes() == []) + } + + /// Padding must be zeroed, not left as whatever was in the buffer. + /// Upstream masks the trailing word for the same reason. + @Test func zeroesPaddingBytes() { + var parcel = Parcel() + parcel.append(bytes: [0xFF]) + #expect(parcel.bytes == [0x01, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00]) + } + + // MARK: - Truncation + + @Test func reportsEndOfParcelRatherThanReturningGarbage() { + let parcel = Parcel(bytes: [0x01, 0x00]) + var reader = parcel.reader() + #expect(throws: ParcelError.endOfParcel) { try reader.readInt32() } + } + + @Test func reportsEndOfParcelWhenStringRunsPastTheEnd() { + // Claims 100 code units in an 8-byte parcel. + let parcel = Parcel(bytes: [0x64, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00]) + var reader = parcel.reader() + #expect(throws: ParcelError.endOfParcel) { try reader.readString() } + } + + @Test func reportsInvalidLengthForNegativeByteArray() { + let parcel = Parcel(bytes: [0xFE, 0xFF, 0xFF, 0xFF]) + var reader = parcel.reader() + #expect(throws: ParcelError.invalidLength(-2)) { try reader.readBytes() } + } + + /// A reader must not advance past a failed read, or a caller that catches + /// and retries would see a cursor pointing into the middle of a value. + @Test func leavesCursorUnmovedAfterFailedRead() { + let parcel = Parcel(bytes: [0x01, 0x00]) + var reader = parcel.reader() + #expect(throws: ParcelError.endOfParcel) { try reader.readInt32() } + #expect(reader.offset == 0) + } + + // MARK: - A realistic exchange + + /// The shape every Waydroid call has: a status header, then the payload. + @Test func roundTripsAnAIDLStyleReply() throws { + var reply = Parcel() + reply.append(Int32(0)) // exception code: none + reply.append("com.example") // a returned string + reply.append(Int32(7)) + reply.append(true) + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + #expect(try reader.readString() == "com.example") + #expect(try reader.readInt32() == 7) + #expect(try reader.readBool() == true) + #expect(reader.isAtEnd) + } +} diff --git a/Tests/BinderTests/ReferenceCountingTests.swift b/Tests/BinderTests/ReferenceCountingTests.swift new file mode 100644 index 0000000..3f03b1e --- /dev/null +++ b/Tests/BinderTests/ReferenceCountingTests.swift @@ -0,0 +1,192 @@ +// +// ReferenceCountingTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +/// The parts of reference counting that hold without a driver. +/// +/// The commands themselves need a device, but what a reference count *depends* +/// on — that objects are found at declared offsets, that a reply's objects can +/// be enumerated, and that the acquire commands are encoded where the driver +/// expects them — is checkable here, and is where the mistakes are. +@Suite +struct ReferenceCountingTests { + + // MARK: - Offsets + + /// The driver rewrites handles only at positions the transaction names. An + /// object written without its offset recorded crosses as plain data and + /// arrives as a number addressing something else in the receiver. + @Test func recordsAnObjectsOffset() { + var parcel = Parcel() + parcel.append(Int32(1)) + parcel.append(RemoteObject(handle: 9)) + + #expect(parcel.objectOffsets == [4]) + #expect(parcel.count == 4 + MemoryLayout.size) + } + + @Test func recordsSeveralObjectsInWireOrder() { + var parcel = Parcel() + parcel.append("name") + parcel.append(RemoteObject(handle: 1)) + parcel.append(Int32(0)) + parcel.append(RemoteObject(handle: 2)) + + #expect(parcel.objectOffsets.count == 2) + #expect(parcel.objectOffsets == parcel.objectOffsets.sorted()) + #expect(parcel.objects == [RemoteObject(handle: 1), RemoteObject(handle: 2)]) + } + + /// Plain data must not be mistaken for an object, however much it looks + /// like one. Only declared offsets count. + @Test func doesNotFindObjectsThatWereNotDeclared() { + var parcel = Parcel() + // The bit pattern of a real flat_binder_object, written as raw bytes. + var object = flat_binder_object() + object.hdr = binder_object_header(type: .handle) + object.handle = 7 + parcel.append(bitPattern: object) + + #expect(parcel.objectOffsets.isEmpty) + #expect(parcel.objects.isEmpty) + } + + @Test func parcelsWithoutObjectsDeclareNoOffsets() { + var parcel = Parcel() + parcel.append(Int32(1)) + parcel.append("two") + parcel.append(bytes: [3]) + #expect(parcel.objectOffsets.isEmpty) + #expect(parcel.objects.isEmpty) + } + + /// An offsets array from another process is not trusted: one naming a + /// position that runs off the end must not send the object reader past it. + @Test func ignoresOffsetsThatRunPastTheEnd() { + let parcel = Parcel(bytes: [UInt8](repeating: 0, count: 8), objectOffsets: [4]) + #expect(parcel.objects.isEmpty) + } + + @Test func ignoresNegativeOffsets() { + let parcel = Parcel( + bytes: [UInt8](repeating: 0, count: 64), + objectOffsets: [-8] + ) + #expect(parcel.objects.isEmpty) + } + + // MARK: - Which objects hold references + + /// Only handles are references this process owns. A local binder pointer, + /// a file descriptor or a buffer is not something to acquire — and taking + /// the handle out of the union would produce a number addressing something + /// unrelated. + @Test(arguments: [BinderType.fileDescriptor, .fileDescriptorArray, .pointer, .binder]) + func doesNotTreatNonHandlesAsReferences(_ type: BinderType) { + var object = flat_binder_object() + object.hdr = binder_object_header(type: type) + object.handle = 5 + + var parcel = Parcel() + parcel.recordObjectOffset() + parcel.append(bitPattern: object) + + #expect(parcel.objects.isEmpty) + } + + @Test(arguments: [BinderType.handle, .weakHandle]) + func treatsHandlesAsReferences(_ type: BinderType) { + var object = flat_binder_object() + object.hdr = binder_object_header(type: type) + object.handle = 5 + + var parcel = Parcel() + parcel.recordObjectOffset() + parcel.append(bitPattern: object) + + #expect(parcel.objects == [RemoteObject(handle: 5)]) + } + + // MARK: - Command encoding + + /// All four reference commands carry a bare handle, not a structure. + @Test(arguments: [ + DriverCommandProtocol.acquire, + .release, + .incRefs, + .decRefs, + ]) + func referenceCommandsCarryABareHandle(_ command: DriverCommandProtocol) { + #expect(command.payloadSize == MemoryLayout.size) + + var stream = CommandStream() + stream.append(command, payload: UInt32(0x2A)) + #expect(stream.count == 4 + 4) + + let handle = Array(stream.bytes.dropFirst(4)) + .withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } + #expect(handle == 0x2A) + } + + /// Acquires must precede the free in the stream the driver consumes. + /// + /// The driver ties the reference it granted for each object to the buffer + /// and drops it in `binder_transaction_buffer_release`, so a free that went + /// first would leave every handle in the reply already dead — and reusable + /// by the driver for a different object. + @Test func acquiresAreOrderedBeforeTheFree() { + var reply = Parcel() + reply.append(RemoteObject(handle: 1)) + reply.append(RemoteObject(handle: 2)) + + var stream = CommandStream() + for object in reply.objects { + stream.append(.acquire, payload: object.handle) + } + stream.append(.freeBuffer, payload: binder_uintptr_t(0xF00D)) + + var offset = 0 + func nextCode() -> UInt32 { + let code = Array(stream.bytes[offset ..< offset + 4]) + .withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } + offset += 4 + return code + } + + #expect(nextCode() == BC_ACQUIRE.rawValue) + offset += DriverCommandProtocol.acquire.payloadSize + #expect(nextCode() == BC_ACQUIRE.rawValue) + offset += DriverCommandProtocol.acquire.payloadSize + #expect(nextCode() == BC_FREE_BUFFER.rawValue) + } + + // MARK: - addService + + /// Registration carries the object inline, so its offset has to be declared + /// or the service manager receives a handle meaningless in its process. + @Test func addServiceRequestDeclaresTheObjectsOffset() throws { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append("example") + request.append(RemoteObject(handle: 3)) + request.append(false) + request.append(ServiceManager.DumpPriority.default.rawValue) + + #expect(request.objectOffsets.count == 1) + #expect(request.objects == [RemoteObject(handle: 3)]) + + var reader = request.reader() + #expect(try reader.readInterfaceToken() == ServiceManager.descriptor) + #expect(try reader.readString() == "example") + #expect(try reader.readObject() == RemoteObject(handle: 3)) + #expect(try reader.readBool() == false) + #expect(try reader.readInt32() == 0b1000) + #expect(reader.isAtEnd) + } +} diff --git a/Tests/BinderTests/ServiceManagerTests.swift b/Tests/BinderTests/ServiceManagerTests.swift new file mode 100644 index 0000000..c657391 --- /dev/null +++ b/Tests/BinderTests/ServiceManagerTests.swift @@ -0,0 +1,214 @@ +// +// ServiceManagerTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +@Suite +struct ServiceManagerTests { + + // MARK: - RPC header + + /// The receiver compares this value and rejects the whole transaction on a + /// mismatch, so the packing is pinned here rather than trusted. + @Test func packsHeaderMarkersAsFourCharacters() { + #expect(RPCHeader.system.value == 0x5359_5354) // 'S','Y','S','T' + #expect(RPCHeader.vendor.value == 0x564E_4452) // 'V','N','D','R' + #expect(RPCHeader.recovery.value == 0x5245_434F) // 'R','E','C','O' + #expect(RPCHeader.unknown.value == 0x554E_4B4E) // 'U','N','K','N' + } + + /// A client driving Android services wants SYST. An off-Android libbinder + /// build would send UNKN, which those services reject — the distinction is + /// the whole reason this is configurable. + @Test func headersAreAllDistinct() { + let values = Set(RPCHeader.allCases.map(\.value)) + #expect(values.count == RPCHeader.allCases.count) + } + + // MARK: - Interface token + + /// Four fields in a fixed order before the descriptor. Getting the count + /// wrong shifts every argument and the callee rejects the request before + /// looking at them. + @Test func writesTokenFieldsInOrder() throws { + var parcel = Parcel() + parcel.appendInterfaceToken("android.os.IServiceManager") + + var reader = parcel.reader() + #expect(try reader.readInt32() == Parcel.strictModePenaltyGather) + #expect(try reader.readInt32() == Parcel.unsetWorkSource) + #expect(try reader.readInt32() == RPCHeader.system.value) + #expect(try reader.readString() == "android.os.IServiceManager") + #expect(reader.isAtEnd) + } + + /// STRICT_MODE_PENALTY_GATHER is 1 << 31, which is negative in the Int32 + /// the parcel carries. Writing it as a UInt32 would be a different value. + @Test func strictModeGatherIsTheSignBit() { + #expect(Parcel.strictModePenaltyGather == Int32.min) + #expect(Parcel.unsetWorkSource == -1) + } + + @Test func orsTheCallersStrictModePolicyIntoTheFlag() throws { + var parcel = Parcel() + parcel.appendInterfaceToken("x", strictModePolicy: 0x11) + + var reader = parcel.reader() + #expect(try reader.readInt32() == (0x11 | Int32.min)) + } + + @Test func roundTripsAToken() throws { + var parcel = Parcel() + parcel.appendInterfaceToken("android.os.IServiceManager") + + var reader = parcel.reader() + #expect(try reader.readInterfaceToken() == "android.os.IServiceManager") + } + + /// A mismatched marker is reported distinctly, because it means the two + /// sides were built against different binder configurations rather than + /// that the parcel is corrupt. + @Test func rejectsAMismatchedMarker() { + var parcel = Parcel() + parcel.appendInterfaceToken("x", header: .vendor) + + var reader = parcel.reader() + #expect(throws: ParcelError.interfaceMismatch( + expected: RPCHeader.system.value, + found: RPCHeader.vendor.value + )) { + try reader.readInterfaceToken(header: .system) + } + } + + // MARK: - Transaction codes + + /// Positional, from the declaration order in IServiceManager.aidl. Not + /// values anyone chose, and not stable under reordering upstream. + @Test func transactionCodesMatchTheAIDLOrder() { + #expect(ServiceManager.Transaction.getService.rawValue == 1) + #expect(ServiceManager.Transaction.checkService.rawValue == 2) + #expect(ServiceManager.Transaction.addService.rawValue == 3) + #expect(ServiceManager.Transaction.listServices.rawValue == 4) + } + + @Test func descriptorIsTheAIDLInterfaceName() { + #expect(ServiceManager.descriptor == "android.os.IServiceManager") + } + + @Test func contextManagerIsHandleZero() { + #expect(RemoteObject.contextManager.handle == 0) + } + + // MARK: - Dump priorities + + @Test func dumpPriorityAllCoversEveryFlag() { + #expect(ServiceManager.DumpPriority.all.rawValue == 0b1111) + #expect(ServiceManager.DumpPriority.all.contains(.critical)) + #expect(ServiceManager.DumpPriority.all.contains(.high)) + #expect(ServiceManager.DumpPriority.all.contains(.normal)) + #expect(ServiceManager.DumpPriority.all.contains(.default)) + } + + // MARK: - Remote objects + + /// A flat_binder_object is 24 bytes and is not length-prefixed, so the + /// reader has to know the size exactly. + @Test func remoteObjectIsAFlatBinderObject() throws { + var parcel = Parcel() + parcel.append(RemoteObject(handle: 42)) + #expect(parcel.count == MemoryLayout.size) + + var reader = parcel.reader() + #expect(try reader.readObject() == RemoteObject(handle: 42)) + #expect(reader.isAtEnd) + } + + /// A null binder is how the service manager says "no such service", so it + /// has to decode as absence rather than as handle zero — which is the + /// context manager and very much a real object. + @Test func readsANullBinderAsAbsent() throws { + var object = flat_binder_object() + object.hdr = binder_object_header(type: .binder) + object.binder = 0 + object.cookie = 0 + + var parcel = Parcel() + parcel.append(bitPattern: object) + + var reader = parcel.reader() + #expect(try reader.readObject() == nil) + } + + @Test func readsAFileDescriptorObjectAsNotARemoteObject() throws { + var object = flat_binder_object() + object.hdr = binder_object_header(type: .fileDescriptor) + object.handle = 3 + + var parcel = Parcel() + parcel.append(bitPattern: object) + + var reader = parcel.reader() + #expect(try reader.readObject() == nil) + } + + // MARK: - Whole requests + + /// The exact bytes a getService call puts on the wire. + @Test func encodesAGetServiceRequest() throws { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append("activity") + + var reader = request.reader() + #expect(try reader.readInterfaceToken() == "android.os.IServiceManager") + #expect(try reader.readString() == "activity") + #expect(reader.isAtEnd) + } + + @Test func encodesAListServicesRequest() throws { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append(ServiceManager.DumpPriority.all.rawValue) + + var reader = request.reader() + #expect(try reader.readInterfaceToken() == "android.os.IServiceManager") + #expect(try reader.readInt32() == 0b1111) + #expect(reader.isAtEnd) + } + + /// The reply shape both lookups decode: exception code, then the object. + @Test func decodesAGetServiceReply() throws { + var reply = Parcel() + reply.append(Int32(0)) + reply.append(RemoteObject(handle: 7)) + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + #expect(try reader.readObject() == RemoteObject(handle: 7)) + } + + @Test func decodesAListServicesReply() throws { + var reply = Parcel() + reply.append(Int32(0)) + reply.append(Int32(2)) + reply.append("activity") + reply.append("package") + + var reader = reply.reader() + #expect(try reader.readInt32() == 0) + let count = try reader.readInt32() + #expect(count == 2) + var names = [String]() + for _ in 0 ..< count { + if let name = try reader.readString() { names.append(name) } + } + #expect(names == ["activity", "package"]) + #expect(reader.isAtEnd) + } +} diff --git a/Tests/BinderTests/ServingLifecycleTests.swift b/Tests/BinderTests/ServingLifecycleTests.swift new file mode 100644 index 0000000..5913a06 --- /dev/null +++ b/Tests/BinderTests/ServingLifecycleTests.swift @@ -0,0 +1,100 @@ +// +// ServingLifecycleTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +@Suite +struct ServingLifecycleTests { + + // MARK: - Registering a hosted object + + /// Publishing a hosted object writes it as a node, so the service manager + /// receives a handle pointing back here rather than a forwarded reference. + @Test func addServiceForALocalObjectSendsANode() throws { + let registry = LocalObjectRegistry() + let object = registry.register(interfaceDescriptor: "com.example.IThing") { _ in nil } + + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append("example") + request.append(object) + request.append(false) + request.append(ServiceManager.DumpPriority.default.rawValue) + + #expect(request.objectOffsets.count == 1) + // A hosted object is a node, not a remote reference, so it must not + // turn up as something to acquire. + #expect(request.objects.isEmpty) + + let flat = request.bytes[request.objectOffsets[0]...] + .withUnsafeBytes { $0.loadUnaligned(as: flat_binder_object.self) } + #expect(flat.hdr.type == BinderType.binder.rawValue) + #expect(flat.binder == binder_uintptr_t(object.id)) + } + + /// A forwarded reference is written as a handle, which is the difference + /// that decides whether the driver keeps the node here or rewrites it. + @Test func addServiceForARemoteObjectSendsAHandle() throws { + var request = Parcel() + request.appendInterfaceToken(ServiceManager.descriptor) + request.append("example") + request.append(RemoteObject(handle: 9)) + request.append(false) + request.append(ServiceManager.DumpPriority.default.rawValue) + + #expect(request.objects == [RemoteObject(handle: 9)]) + let flat = request.bytes[request.objectOffsets[0]...] + .withUnsafeBytes { $0.loadUnaligned(as: flat_binder_object.self) } + #expect(flat.hdr.type == BinderType.handle.rawValue) + } + + // MARK: - Thread exit + + @Test func threadExitIsAScopedCommand() { + #expect(BinderCommand.threadExit.rawValue == numericCast(BINDER_THREAD_EXIT)) + } + + // MARK: - Security-context transactions + + /// binder_transaction_data_secctx is a plain transaction with a context + /// pointer appended, and the transaction is at offset zero. If that layout + /// changed, dispatching the prefix would read a shifted structure. + @Test func secctxWrapsAPlainTransactionAtOffsetZero() { + #expect(MemoryLayout.size + == MemoryLayout.size + + MemoryLayout.size) + #expect(MemoryLayout.offset(of: \binder_transaction_data_secctx.transaction_data) == 0) + } + + @Test func secctxAndPlainTransactionAreDistinctReturns() { + #expect(DriverReturnProtocol.transaction != DriverReturnProtocol.transactionSecCtx) + #expect(DriverReturnProtocol.transactionSecCtx.payloadSize + == MemoryLayout.size) + #expect(DriverReturnProtocol.transaction.payloadSize + == MemoryLayout.size) + } + + /// The prefix of a secctx structure decodes as the same transaction a plain + /// BR_TRANSACTION would carry, which is what lets one dispatch path serve + /// both. + @Test func secctxPrefixDecodesAsThePlainTransaction() { + var wrapped = binder_transaction_data_secctx() + wrapped.transaction_data.code = 5 + wrapped.transaction_data.sender_pid = 4321 + wrapped.transaction_data.sender_euid = 10099 + wrapped.secctx = 0xDEAD_BEEF + + let bytes = withUnsafeBytes(of: wrapped) { Array($0) } + let prefix = bytes.prefix(MemoryLayout.size) + .withUnsafeBytes { $0.loadUnaligned(as: binder_transaction_data.self) } + + #expect(prefix.code == 5) + #expect(prefix.sender_pid == 4321) + #expect(prefix.sender_euid == 10099) + } +} diff --git a/Tests/BinderTests/WeakReferenceTests.swift b/Tests/BinderTests/WeakReferenceTests.swift new file mode 100644 index 0000000..23ff87a --- /dev/null +++ b/Tests/BinderTests/WeakReferenceTests.swift @@ -0,0 +1,67 @@ +// +// WeakReferenceTests.swift +// Binder +// + +import CBinder +import Testing + +@testable import Binder + +@Suite +struct WeakReferenceTests { + + /// Weak and strong references travel as distinct commands. Crossing them + /// would keep an object alive that should be collectable, or fail to keep + /// one alive that a caller is relying on. + @Test func weakAndStrongUseDistinctCommands() { + #expect(DriverCommandProtocol.incRefs != DriverCommandProtocol.acquire) + #expect(DriverCommandProtocol.decRefs != DriverCommandProtocol.release) + } + + /// All four reference commands carry a bare handle. + @Test(arguments: [ + DriverCommandProtocol.incRefs, + .decRefs, + .acquire, + .release, + ]) + func referenceCommandsCarryABareHandle(_ command: DriverCommandProtocol) { + #expect(command.payloadSize == MemoryLayout.size) + + var stream = CommandStream() + stream.append(command, payload: UInt32(11)) + let handle = Array(stream.bytes.dropFirst(4)) + .withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } + #expect(handle == 11) + } + + // MARK: - Extended error + + /// binder_extended_error is three 32-bit fields: an id, the failed BR_* + /// command, and a status. If the layout drifted, a diagnostic read would + /// report the wrong command for a failure. + @Test func extendedErrorHasThreeWords() { + #expect(MemoryLayout.size == 12) + #expect(BinderCommand.getExtendedError.rawValue + == numericCast(BINDER_GET_EXTENDED_ERROR)) + } + + /// The typed view of the failed-command field reads back the raw BR_* code. + @Test func extendedErrorTypesTheFailedCommand() { + var error = ExtendedError() + error.command = BR_FAILED_REPLY.rawValue + error.param = -22 // -EINVAL + + #expect(error.failedCommand == .failedReply) + #expect(error.status == -22) + } + + /// The initial state before any failure describes success, which is why + /// the value is only meaningful straight after a failure on the same + /// thread. + @Test func extendedErrorDefaultsToSuccess() { + let error = ExtendedError() + #expect(error.status == 0) + } +}