Migrating from 0.11 to 0.12
Every change in 0.12.0 that can break a program built against 0.11.0: a compile error, a different value on the wire, or behavior a running program can observe. Entries are grouped by area; within an area, those that change the wire say so and need both peers upgraded together.
The additions and fixes themselves are listed in
CHANGELOG.md;
a reference such as "(CHANGELOG Fixed)" points to the 0.12.0 section there.
Toolchain and features
- Minimum supported Rust version is now 1.86 (was 1.85). The local-binder
cast
Binder::<B>::try_from(SIBinder)(behindFromIBinder::try_fromandSIBinder::into_interface) now downcasts theArcthrough trait upcasting instead of reinterpreting a raw pointer, which needs 1.86. Results are unchanged: a remote proxy, anotherRemotabletype, or anIBinderwrapper that forwardsas_any()to a local binder still fails withBadValue. - Android builds must link for API 29 (Android 10, the oldest supported
platform) or newer: pass
--platform 29tocargo ndk. rsbinder now calls bionic'sprocess_vm_readv, which cargo-ndk's default API 21 does not export, so a build without it fails withundefined symbol: process_vm_readv. 0.11.0 linked at API 21. - The
tokiofeature no longer enablestokio/full, onlytokio/rt,tokio/rt-multi-threadandtokio/sync(fordeath_signal). A crate that receivedtokio/net,time,signal,fs,io-utilormacrosthrough rsbinder must declare them in its ownCargo.toml. BinderAsyncRuntime for TokioRuntime<Runtime>andfor TokioRuntime<Arc<Runtime>>are gone (an owned runtime in a binder panics inRuntime::dropwhen the lastStrongdrops on a worker). Wrapruntime.handle().clone()inTokioRuntime<Handle>instead.
Status codes
StatusCode::from(ExceptionCode::ServiceSpecific)isServiceSpecific(0), notOk, as in AOSP;StatusCode::from(status)for such aStatusnow yieldsServiceSpecific(0), notFailedTransaction.- On a host whose errno numbering is not Linux asm-generic (Apple
platforms, and Linux on SPARC, MIPS, Alpha or PA-RISC) the status codes on
the wire changed; upgrade every such peer together. 0.12.0 sends AOSP's
values (
utils/Errors.hin Linux asm-generic errno numbering) on every host (CHANGELOG Fixed). Between a 0.11.0 and a 0.12.0 macOS peer six named codes decode differently: a 0.11.0 peer reads 0.12.0'sInvalidOperation,UnknownTransaction,BadIndex,NotEnoughData,WouldBlockandTimedOutasErrnovalues (NotEnoughData's-61as DarwinECONNREFUSED), and a 0.12.0 peer reads 0.11.0's asUnknown. AStatusCode::Errnono longer crosses between two such peers either: it arrives asUnknown. The same holds for aStatusCodeserialized into aParcel. Between two peers with asm-generic numbering (Linux and Android on every other architecture) these codes do not change. - On a host whose errno numbering is not Linux asm-generic
StatusCode::from(i32)andi32::from(StatusCode)convert wire values, not host errnos.StatusCode::from(-libc::ECONNREFUSED)isNotEnoughDataon macOS, since-61is AOSPNOT_ENOUGH_DATA;StatusCode::from(-libc::ECONNRESET)isUnknown, andi32::from(StatusCode::Errno(e))isUNKNOWN_ERROR(i32::MIN). Build a status from an errno withFrom<rustix::io::Errno>orFrom<std::io::Error>, which keepStatusCode::Errnoin the host's numbering. On a host with asm-generic numbering both conversions are unchanged apart from the sign rule in the next entry. - A handler's
Err(StatusCode::Errno(x))withx >= 0, orErr(StatusCode::ServiceSpecific(v))withv <= 0, now answersUnknownon every host (CHANGELOG Fixed), where it answered success (0) or a status the client decoded as another variant.i32::fromand aStatusCodewritten into aParcelfollow the same rule. Send a zero or negative service-specific code inside aStatus(Status::new_service_specific_error,Status::service_specific), which carries it unchanged. ExceptionCode::from(StatusCode::UnexpectedNull)isTransactionFailed, notNullPointer, as AOSPStatus::setFromStatusTmaps every non-zerostatus_t. A handler returningErr(StatusCode::UnexpectedNull)now answers with that transaction status instead of anEX_NULL_POINTERexception header: the client'sStatushasexception_code() == TransactionFailedandtransaction_error() == UnexpectedNull, and a branch onExceptionCode::NullPointerno longer runs.- A service handler's
Err(StatusCode::DeadObject)reaches a remote caller asFailedTransaction, over kernel binder and over RPC; every other status goes out asi32::fromencodes it. An AOSPBpBinderthat receivesDEAD_OBJECTas a reply status marks itself dead (mAlive = 0,BpBinder::transact) and fails every later call on that proxy without sending it, so one handler error cut the caller off from a service that was still running.DeadObjectreaches a handler as an ordinary value:?on a nested call to a dead binder, or on aBrokenPipewrite error (EPIPEisDEAD_OBJECT), as in anon_dumpwhose reader exited (dumpsys foo | head). The substitution also covers aStatuswithExceptionCode::TransactionFailedandDeadObjectas its transaction error, which a generated stub sends as the reply status. A local (in-process) call still returns the handler'sErrunchanged; a client that matchedDeadObjectfrom a live rsbinder service now seesFailedTransaction. FLAG_COLLECT_NOTED_APP_OPSis0x2, not0x80(CHANGELOG Fixed): a flag value sent on the wire changes, so a 0.11.0 peer testing or setting it disagrees with a 0.12.0 one; rebuild both sides.
Entry API (serve / connect)
-
serve,connect,connect_binder,connect_async,Client::openandClient::open_withtakeimpl Into<Uri>, not&str(CHANGELOG Changed). Build the destination with constructors, or parse a string explicitly:#![allow(unused)] fn main() { // 0.11.0 rsbinder::serve("binder://")?; let h: Strong<dyn IHello> = rsbinder::connect("unix:///tmp/x.sock#hello")?; let h: Strong<dyn IHello> = rsbinder::connect(&config.target)?; // 0.12.0 rsbinder::serve(Uri::kernel())?; let h: Strong<dyn IHello> = rsbinder::connect(Endpoint::unix("/tmp/x.sock").with_service("hello"))?; let h: Strong<dyn IHello> = rsbinder::connect(config.target.parse::<Uri>()?)?; }The schemes and query keys are the same, but a malformed string now fails at
parsewith aUriErrorinstead of atserve/connect;?intorsbinder::Resultstill yieldsStatusCode::BadValueand logs the reason. Replace a URI built withformat!("unix://{}", path.display())byEndpoint::unix(&path): the string form is percent-decoded, so a path holding%,#or?named a different socket or failed to parse. -
Endpointchanged shape.Endpoint::Kernel(KernelEndpoint)replaces the 0.11.0 struct variantKernel { driver, threads }; read the settings withKernelEndpoint::driver()/threads()/mmap_size()and build one withKernelEndpoint::default().with_threads(..).Endpoint::Vsock { cid, port }andEndpoint::Tls { host, port }replace the tuple variants; build them withEndpoint::vsock(cid, port)andEndpoint::tls(host, port). -
Uri's fields are private: useendpoint(),service()andwire().wire_max_version: Option<u32>is now aWireProfile—R34, orAndroid13Plus(WireVersion)for?profile=android13plus[-vN].rsbinder::entry::uri::parseis removed; useUri::parseorstr::parse. -
A
tls://host is percent-decoded, its[ ], if any, must be one pair enclosing the whole host, and a host holding:needs them. 0.11.0 took the host text literally, split it from the port at the last:and stripped any number of brackets from its ends:tls://[[::1]]:9000,tls://[::1:9000andtls://::1:9000reached::1,tls://a]:9000reacheda, andtls://fe80::1, with no port, became hostfe80:and port 1. All of these are nowUriErrorKind::BadBrackets; writetls://[::1]:9000. Write an IPv6 zone id as%25(tls://[fe80::1%25eth0]:9000):tls://[fe80::1%eth0]:9000is nowBadPercentEscape, and a numeric zone such as%12decodes to a control character instead of reaching the resolver. A client connecting to a host with a zone id must also setClientOptions::tls_server_name: the host is the default TLS server name, a server name cannot hold a zone id, and without it the connect fails withStatusCode::RpcError.tls://[]:9000, which 0.11.0 took as an empty host, isEmptyHost. -
A query key given twice is refused (
UriErrorKind::DuplicateQueryKey). 0.11.0 used the last value, sounix:///a?profile=android13plus-v2&profile=android13plus-v0spoke v0. -
A
?queryafter#serviceis refused (UriErrorKind::QueryAfterService). 0.11.0 made it part of the service name:binder://#svc?driver=/dev/xlooked up"svc?driver=/dev/x"on the default driver. Put the query first. -
A
unix://path or?driver=may hold percent-escaped bytes that are not UTF-8; 0.11.0 refusedunix:///tmp/%FF.sock. A kernel driver path that is not UTF-8 parses but isBadValueatserve/connect, sinceProcessStateopens the driver by a&str. -
?profile=android13plus-vNaccepts any decimal spelling ofN, so-v02is version 2; 0.11.0 matched only-v0,-v1and-v2. -
A kernel
Server::addno longer registers the name;run/spawndo, in this order: check theServeOptions, start the thread pool, register the names. 0.11.0 registered atadd, so an option refused atrun/spawnleft the name published with no thread to serve it, and its callers blocked. Two consequences:-
A readiness signal printed between
addandrunnow comes before the name exists, and a client that looks it up once (check_service) can miss it. Signal afterspawninstead:#![allow(unused)] fn main() { // 0.11.0 let server = rsbinder::serve(Uri::kernel())?.add(NAME, svc)?; println!("READY"); server.run() // 0.12.0 let _server = rsbinder::serve(Uri::kernel())?.add(NAME, svc)?.spawn()?; println!("READY"); ProcessState::join_thread_pool() } -
A registration error — a name the service manager refuses (invalid, or held by another uid under
rsb_hub), a policy denial, an RPC proxy — is returned byrun/spawn, notadd. Names registered before the failing one stay published and served until the process exits.
-
Kernel binder, Parcel and process state
- A kernel option
serve/Client::opencannot honor is nowBadValue. TheKernelEndpointthread count and driver (binder://?threads=,?driver=),ServeOptions::threadsandClientOptions::driverare fixed by whoever initializesProcessStatefirst; a later different value used to log a warning and now returnsStatusCode::BadValue(omitting it or repeating the value still works). LikewiseClientOptions::driver/mmap_sizemust agree with theKernelEndpoint's driver and mapping size. A driver path that is not UTF-8 isBadValuetoo; 0.11.0 opened its lossy UTF-8 conversion, a different path. ServerGuardandServerare#[must_use]:serve(uri)?.spawn()?;stopped an RPC server at once. Bind the guard to a named variable.- A kernel transaction dispatched inside an RPC handler now reports the
kernel caller:
calling_caller(),get_calling_uid()/pid()/sid()andCallingContext::default()returnCaller::Kerneland the kernel sender's values, not the suspended RPC peer's. Re-check handlers that pick an authorization rule bymatching on theCallerarm. - Outside a transaction
get_calling_uid()/get_calling_pid()return this process's own uid and pid instead of0(root), as AOSP'sIPCThreadStatedoes. Inside a transaction nothing changes. ProcessState::set_call_restrictionnow restricts the calling thread too (CHANGELOG Fixed). A thread that had already made a binder call kept its previous value; it now takes the new one at once, so a twoway call it makes afterwards logs an error (ErrorIfNotOneway) or panics (FatalIfNotOneway). The main looper ofServeOptions::call_restrictionis such a thread: a handler running on it that makes a twoway call now hits the restriction.- A kernel
Client::get/connectreports the service manager's own failure instead ofNameNotFoundwhen it cannot be reached, andhublogs why once. get_calling_sid()isNonewhere SELinux is not available (CHANGELOG Fixed): on Linux without a mounted selinuxfs, Smack and AppArmor hosts included, a binder withBinderFeatures::set_requesting_sidno longer asks the driver for the caller's context. Under Smack or AppArmor 0.11.0 returned the label, read on to the next NUL byte.Parcelis no longerRefUnwindSafewithout therpcfeature: a kernel parcel now holds the binder proxies written into it (CHANGELOG Fixed). Withrpcit already was not. Wrap a&ParcelinAssertUnwindSafewherecatch_unwindneeds it.- A parcel write over a binder or fd object it recorded is
PermissionDenied(CHANGELOG Fixed): rewinding withset_data_positionand writing over an object fails where 0.11.0 overwrote it. Parcel::from_ipc_partshas one more# Safetycondition: the handle of everyBINDER_TYPE_FDobject at an offset inobjectsmust be an open file descriptor that stays open until the parcel drops. Reading such an object duplicates that fd, as 0.11.0 already did, and the parcel closes none of them; the signature and behavior are unchanged. Under the 0.11.0 conditions a buffer naming a closed fd number was allowed, and a read then duplicated whatever fd that number named by then.Parcel::__set_for_rpc(rpc+test-util) returnsResult<()>and refuses, withInvalidOperationand the mode left unchanged, a parcel that holds bytes or was built over a received buffer (CHANGELOG Fixed). Call it on a new parcel before writing to it.- Over kernel binder, a service's
on_dumpwrites to a duplicate of the caller's fd, not to the received fd itself (CHANGELOG Fixed). The duplicate is made before the arguments are read; a process out of file descriptors now answers theDUMP_TRANSACTIONwith that error instead of runningon_dump. Over RPC,on_dumpnow runs as well (CHANGELOG Fixed). ProxyHandle::dumptakesF: Into<OwnedFd>, notF: IntoRawFd. AFile, anOwnedFd, aParcelFileDescriptor, aChildStdinand the other owning std types compile unchanged. A raw fd number, or a type that implements onlyIntoRawFd, has to become anOwnedFdfirst (unsafe { OwnedFd::from_raw_fd(fd) }, with the caller vouching that it ownsfd).IntoRawFdis a safe trait that need not hand over ownership, anddumpcloses the fd it takes, so 0.11.0 would close an fd such an implementation had only lent it.IMemoryHeaphas a requiredheap_fd(&self) -> Option<BorrowedFd<'_>>, andheap_idis now a provided method returning that fd's number (-1forNone). An implementation outside rsbinder no longer compiles until it addsheap_fd: return the heap's fd (Some(self.fd.as_fd())), orNonewhile it has none. An ownheap_idmay stay, but nothing reads it for the reply:BnMemoryHeapanswersHEAP_IDwith a duplicate ofheap_fd(andBadValueforNone). In 0.11.0 it borrowed whatever number the safeheap_idmethod returned as an fd, so an implementation returning0or a closed number let one remoteHEAP_IDrequest send an unrelated fd of the process (stdin, say) to the peer. Callers ofheap_id, and the wire, are unchanged.
RPC wire
- The r34 RPC wire (the default profile) now frames messages as android-12
libbinder does; a 0.12.0 peer and an older one no longer connect. A
client writes the
int32session id-1when it connects, then every message as a bareRpcWireHeaderand its body, with nou32length prefix. An older server reads the-1as a frame length pastMAX_FRAME_LEN, and a 0.12.0 server reads an older client's first length as a session id it does not have; both close the connection. Upgrade both ends together. The new wire was checked against android-12 libbinder on an SDK 31 emulator, with each side as client and as server (example-hello/cpp/run_rpc_r34_interop.sh). Every binder in an r34 parcel, a null one included, is now followed by its stability as android-12 writes it, aCategory(0x0c000001for System,0x00000001for null) whatever the host's SDK; a null binder with a declared level or aCategoryof version 0 is refused withBadType.GET_SESSION_IDon r34 answers android-12'sint32, soRpcSession::get_session_idreturns 4 bytes there (was 32); a session not accepted by anRpcServerhas no id and answersUnknownTransaction. A customRpcTransportthat implements onlysend_frame/recv_framecan no longer carry any session:RpcSession::newas a client fails withRpcError::Protocol, as the android-13+ profile always did; implementsend_raw/recv_raw.RpcSession::newas an acceptor reads the id in its firstserve_blockingread and ends the session on any id but-1. - RPC on the android-13+ wire: a null binder now carries a stability
int32, as libbinder's does (CHANGELOG Fixed). An rsbinder 0.11.0 peer on that wire reads the parcel 4 bytes off after a null binder or a null interface, so update both ends together. Parcels without a null binder are unchanged. - An android-13+ RPC session takes its fd mode from the connection header
only, as AOSP does. There
RpcSession::negotiate_fd_transportsends nothing: it returns the header's mode, and aUnixrequest the header did not agree isInvalidOperation; 0.11.0 sentGET_FD_MODEand could switch the session toUnixafter connecting. Request fd passing before connecting withRpcClientConfig::fd_mode(thefd_modeconnect option of the entry API), and on the server withRpcServer::set_supported_fd_modes. A 0.12.0 server answers an android-13+GET_FD_MODEwithUNKNOWN_TRANSACTION, as libbinder does, so a 0.11.0 client callingnegotiate_fd_transporton such a session getsUnknownTransaction. On the r34 wirenegotiate_fd_transportstill negotiates; against a peer withoutGET_FD_MODE(AOSP r34 libbinder answersUNKNOWN_TRANSACTION) it now returnsOk(FileDescriptorTransportMode::None)where 0.11.0 returnedErr(UnknownTransaction), so the first fd write fails (FdsNotAllowed) instead of the negotiation. An android-13 (v0) session has no fd mode in its header and staysNonewhatever both ends request. - An android-13+ RPC server refuses a client that requests an fd mode it does
not support, as AOSP does (
RpcServer.cpp"Rejecting connection: FileDescriptorTransportMode is not supported"):UnixwithoutRpcServer::set_supported_fd_modes(&[Unix])(thefd_modesserve option), orTrusty. The server closes the connection after its handshake response, so the client's setup succeeds and its first call fails withDeadObject. 0.11.0 accepted the client and left only the server atNone: the client's fd writes went out overSCM_RIGHTSand the server dropped the descriptors. Opt in on the server, or connect withoutfd_mode.RpcSession::accept_android13plus_fdwithserver_fd_unix == false(andaccept_android13plus) refuses the same way. The r34 wire is unchanged: itsGET_FD_MODEstill agreesNone. RpcSession::accept_android13plusandaccept_android13plus_fdrefuse a connection header whose session id is neither empty nor 32 bytes, as AOSPRpcServer::establishConnectiondoes, before reading the id; the call returnsStatusCode::RpcError. 0.11.0 read the id and ignored it, so a client that sent a 16-byte id got a session.RpcServeris unchanged: it refused such a connection in 0.11.0 too, when the id matched no session.
RPC sessions and connections
- An RPC session refuses a connection whose transport differs from its
founding one (fd passing or local peer) with
BadTypeat attach. A manual attach (add_{outgoing,incoming}_connection_with_config) over such a transport is refused before its header goes out, so the session stays up; a hand-assembled session (e.g.RpcServer::serve_connection) can hit it too. - An RPC session ends as a whole when any of its connections fails (AOSP
RpcState::handleRpcError). A send or receive failure, a cut frame or an expired deadline on one connection shuts every connection down: calls in flight on the others returnDeadObject, every death recipient fires, and every local object the peer held is released. 0.11.0 retired the failing connection and let a fan-out session go on. After a session ends, reconnect, fetch the root and register callbacks again —rsbinder::Reconnecting(CHANGELOG Added) does this for one service. A client's incoming connection whose serve thread cannot be spawned ends the session too, since the server already holds it as a callback connection. So does every other failed incoming attach, except one whose header never went out (a refused pre-check, a failed connect or header write) or whose connection closed, a reset included, before any byte of the server's"cci"arrived (a refused attach): those leave the session up. A failed outgoing attach (add_outgoing_connection_with_config) ends the session too, since a libbinder server holds the connection once it has the header, except one whose header never went out (a refused pre-check, amax_versionbelow the session's, a transport unlike the founding connection's, an id other than the session'sget_session_id(), a failed connect or header write). A refused attach ends it as well: libbinderandroid-16.0.0_r3and later end their session when they refuse an attach at thesetMaxThreadscap, and the close that refusal produces cannot be told from one that left the server's session up. Stay withinnegotiate()connections and echoget_session_id(). - An expired reply deadline ends the session (
RpcSession::set_timeout,RpcServer::set_reply_timeout, thetimeout/reply_timeoutoptions). The call still returnsTimedOut; the session no longer survives it or skips the late reply. Set it above the slowest legitimate handler; to abandon one call and keep the session, run it on another thread and stop waiting. A wait for a free connection still fails only that call, withWouldBlock. - The session timeout bounds more than the reply wait:
SO_SNDTIMEO(a send that makes no progress for the period ends the session; a steady slow reader is not cut) on every connection of a bundled socket transport, and on a transport of your own only if it implementsRpcTransport::set_write_timeout(without it a stalled send has no bound), the kernel's keepalive check on TCP and TLS over TCP (RpcTransport::set_livenesshas the values and platform differences), and on a client each connect and handshake step (connect(2)itself fortcp_debugandtlsonly, not forunixorvsock). A send deadline that expires part-way through a frame returnsTimedOut(0.11.0:WouldBlock, which means nothing was sent). - A
tlsclient's connect deadline is shared by every address the host name resolves to. In 0.11.0ClientOptions::handshake_timeoutgave each address its own fullconnect(2)deadline, so a host withNaddresses that drop the SYN heldopenforNtimes it. Nowtimeout(or the deprecatedhandshake_timeoutofRpcClientConfigandClientOptions) bounds the tries together: each gets an even share of the time left but at least 2 s (all of it when less is left), an address refused at once leaves its share to the next, and an address slower to accept than its share is given up for the next even when it would have accepted within the deadline. Atimeoutunder 2 s therefore goes to the first address that does not refuse at once. For a host that resolves to several addresses over a link whose connect takes more than the larger oftimeout / Nand 2 s, raise the deadline or connect to one IP address (withtls_server_namenaming the certificate's host). RpcServer::set_handshake_timeoutbounds the whole admission phase, not each read. In 0.11.0 it was a socket deadline (SO_RCVTIMEO,SO_SNDTIMEO) that every read or write started over, so a client whose handshake bytes kept coming was admitted however long they took. Now the connection is dropped once the deadline has passed since its accept, its TLS handshake included and theset_authorizerhook's run left out: a client on a link slow enough to need longer than the deadline (10 s by default) for its whole handshake is refused. On the r34 wire a connection that opens a new session ends the phase only once its first transaction has been served, so a large first transaction counts too; a connection that joins an existing session ends it with its session-id preamble. Raise the deadline for such links. A client's handshake deadline (timeout, or the deprecatedhandshake_timeoutofRpcClientConfig,RpcUnixClientConfigandClientOptions) bounds each TLS or android-13+ handshake step as a whole in the same way: in 0.11.0handshake_timeoutbounded each read, and now a step that outlasts it fails the setup call withTimedOut.RpcSession::from_preconnected_fdbounds its handshake by a fixed 10 s the same way, where 0.11.0 bounded each read by it. No API changes that 10 s: for a peer that needs longer for the whole handshakefrom_preconnected_fdreturnsTimedOut, and a service-manager lookup that resolves through such an Accessor finds no service (the reason is logged).- An expired TLS handshake is
RpcError::Timeout, notIo(WouldBlock):TlsTransport::connect,accept,connect_streamandaccept_streamreturnRpcError::Timeout(StatusCode::TimedOut) when the stream's deadline ends the handshake. 0.11.0 returnedRpcError::Ioof kindWouldBlock. - On Linux and Android a send or read deadline counts the peer taking this
end's bytes as progress (
SIOCOUTQ; thetransportmodule doc "A slow peer"). A consumer that reads a large reply slower thanset_idle_timeout's or the session timeout's period, but keeps reading, is no longer cut mid-send, nor while the tail of a reply whose send returned is still on its way, and a reply wait goes on while the peer is still taking the request. Apple keeps the 0.11.0 rule: a send's progress is a send that accepted bytes, and a read deadline counts only the peer's bytes. So does a socket that refusesSIOCOUTQ(Android SELinux grantsioctlon another domain's socket only where policy says so, and a kernel whose vsock does not report its queue refuses it on a vsock socket); it is asked once, not on every wait. - TCP connections, TLS over TCP included, have keepalive on by default, at the system's intervals without a session timeout (hours), so a session whose peer host vanished ends and fires its death recipients. No wire byte changes.
RpcServer::set_idle_timeoutjudges the session, not the connection: a session ends once no byte has crossed any of its connections and no call has been open for the period, so a fan-out client with one quiet connection is not evicted, and the eviction can come up to one period later than that; theset_idle_timeoutrustdoc states the rule.- A kernel
ETIMEDOUTon an RPC connection is a dead connection, not a deadline of this end's (keepalive orTCP_USER_TIMEOUTgave up): the session ends, and a serve loop reportsNotLocal/Lostinstead of an idle eviction. - RPC
link_to_death(anddeath_signal) is refused withInvalidOperationon a session that would not notice its connection dropping (a client with no incoming connections and no serve loop). Open incoming connections (incoming_connections(n)), or call the newRpcSession::spawn_servebefore linking; the server side is unaffected. - A
onewaycall made inside an RPC handler needs a connection this end opened (CHANGELOG Fixed); without one it isWouldBlock, so a server's oneway callback needs the client's incoming connections. Twoway is unchanged. RpcServer::setup_unix_serverremoves only a stale socket: a regular file is refused withAlreadyExists, a listening socket withEADDRINUSE. Drop removes the socket file only while it is still the one bound. On Apple platforms it removes no socket at all and refuses every existing one withEADDRINUSE, as AOSPsetupUnixDomainServerdoes, so delete a crashed server's socket before restarting: XNU refuses a connect to a live listener whose backlog is full with the sameECONNREFUSEDa stale socket gives, so probing could remove a running server's path. 0.11.0 removed whatever was at the path on every platform.- An RPC
Parcelis sent once (CHANGELOG Fixed): build a new request per call. Sending the same parcel again, or a reply or incoming parcel as a request, returnsInvalidOperation; a parcel built on another session's proxy returnsBadType(AOSPRpcState::validateParcel), as do a local binder written into a sent parcel and aParcelableHolderread on one session written into a parcel of another. In 0.11.0 a scalar-only request could be resent.RpcProxy::transactrefuses a parcel built for no session (Parcel::new()) withBadType; build it withbuild_request. A handler that replaces its reply with such a parcel, or with one built on another session, sends the callerBadType; with a received or already-sent parcel,InvalidOperation. 0.11.0 sent their bytes. - An unresolved
ParcelableHoldercannot be relayed over an r34 or android-13 v0/v1 RPC session; relay requires wire v2 (android-16). Writing a holder that was read but not decoded withget_parcelableinto a parcel of such a session isBadType, even when the payload holds only data: those wires do not record where a binder sits, so the copy could neither take the references its binders need nor prove there are none. Decode the payload first and write the typed value. On v2 the relay carries binders and fds. In 0.11.0 the bytes were copied as they were, and relaying a holder that held one of the peer's own references freed the node early. - A session parcel takes a holder's bytes only from its own session (AOSP
Parcel::appendFrom): a holder read from a kernel parcel, decoded withfrom_bytesor read on another session, written undecoded into an RPC session parcel, isBadType. It was copied as is. - An android-13+ RPC client setup refuses a session id, and an attach
refuses one other than its session's own, with
BadValue. The setup refuses before any handshake a non-empty id passed tosetup_unix_client_android13plus_with_id,setup_unix_client_android13plus_with_config(RpcUnixClientConfig::session_id),connect_android13plus_fd_with_idorClientOptions::session_id, all now deprecated, and likewise the newsetup_client_android13plus_with_configwithRpcClientConfig::session_id. In 0.11.0 they built a second clientRpcSessionon the server session another one founded; the two kept separate oneway numbering, binder addresses and lifetimes, so the server dropped one side's oneway calls as stale, could dispatch a call for one side's binder to the other side's binder at the same address, and ended the shared session when either side closed. AOSP has no public entry for this either (RpcSession::setupClientis private). To add a connection to a session, calladd_outgoing_connection_with_config(oradd_incoming_connection_with_config) on that session withRpcClientConfig::session_id(&session.get_session_id()?); attaching from another process is not supported, as in AOSP. The wire is unchanged: a server still admits an attach that echoes its id. A manual attach (add_outgoing_connection_with_config,add_incoming_connection_with_configand the deprecatedadd_{outgoing,incoming}_connection_android13plus*forms) likewise refuses, withBadValuebefore connecting, every id other than its own session's server-minted one (get_session_id(), which the session now keeps; an attach on a session that never called it makes that round trip itself). 0.11.0 let oneRpcSessionattach a connection to another server session, which split that server session across two client states the same way and pooled connections to two server sessions in one client session. - Several RPC client setup calls are deprecated in favor of
RpcClientConfig(CHANGELOG Deprecated); they still work, except for the session ids now refused (see above), but-D warningsflags them.
rsbinder-aidl
rsbinder-aidlnow rejects.aidlthat AOSP'saidlalso rejects (CHANGELOG Added). An rsbinder-only contract relying on the missing validation now fails, with a diagnostic naming the rule.error::SemanticErroris now#[non_exhaustive]and gainedFixedSizeNonFixedField,VintfStabilityLeakandDirectionNotSpecified; add a_ => …arm.DirectionPrimitiveis replaced byInvalidDirection, which covers every refused direction and adds the argument name and the permitted directions; itstype_kindis the AIDL type (int,String), not"a primitive type".render::ConstMemberandrender::EnumMembergained a trailing deprecation element; passString::new()to keep the previous output. The#[non_exhaustive]render structs' newdeprecatedfield needs no change.rsbinder-aidlparser API:ParcelableDecl::type_paramsandUnionDecl::type_paramsareVec<TypeParam>(name, span, annotations) instead ofVec<String>; readTypeParam::name. NewGeneric::type_args()returns aFoo<A, B>reference's arguments as written.#[deprecated]codegen can break a downstream-D warningsbuild: an existing/** @deprecated … */javadoc now generates#[deprecated]. Drop the tag, or allow the lint at the call site.- A declaration nested in a
@VintfStabilitytype is now VINTF-stable (CHANGELOG Fixed): aParcelableHolderrecords VINTF stability for it, andParcelableHolder::set_parcelablereturnsBadValuefor a payload without VINTF stability. Nothing in the build warns. rsbinder-aidl: a@FixedSizeunion's implicitTagisTag(pub i8)(CHANGELOG Fixed): code that builds or reads it asi32no longer compiles, and aTag[]isbyte[]on the wire. A@FixedSizeunion with more than 128 fields is rejected, because itsbytetag cannot number the 129th; AOSP'saidlrejects it as well.rsbinder-aidl: au32/u64suffix on a hex literal no longer sets its width (CHANGELOG Fixed), as in AOSPParseIntegral:0xFFFFFFFFu64is theint-1, so alongconstant set to it is-1, not4294967295, and0x1FFFFFFFFu32is alonginstead of a parse error. Nothing in the build warns; a constant sent on the wire changes value.rsbinder-aidl:@nullable(heap=true)is no longer ignored. A parcelable or union field marked with it isOption<Box<T>>wherever it is, as AOSP's Rust backend renders it (aidl_to_rust.cpp:305-306); only one on a reference cycle was boxed before, so code building or matching such a field off a cycle asOption<T>stops compiling (wrap the value inBox::new). Such a field no longer counts as an inline edge either, so a bare@nullablefield it cuts off a cycle isOption<T>, notOption<Box<T>>: inparcelable A { @nullable(heap=true) B b; } parcelable B { @nullable A a; },B.ais nowOption<A>.heap=trueon anything but a parcelable or union type (aString, an array, an interface) and any@nullableparameter butheapare refused, with AOSP's messages. The wire is unchanged. Method arguments and returns keep their types.rsbinder-aidl: a union whose first field is an enum without an initializer defaults to the enum'sDefault(backing value0), not to its first enumerator (CHANGELOG Fixed). Forenum E { A = 5, B = 6 }andunion U { E e; … },U::default()isU::E(E::default()), which isE(0), notU::E(E::A); a default-constructed union sent as is carries0on the wire. An enum whose first enumerator is0is unaffected. Nothing in the build warns.rsbinder-aidl/rsbinder-macros: fixed-size array arguments and returns have AOSP's element types (CHANGELOG Fixed). A@nullable T[N]inargument or return value whose element is not a primitive or enum now wraps each element:Option<&[Option<T>; N]>andOption<[Option<T>; N]>, notOption<&[T; N]>andOption<[T; N]>. Aninout T[N]of a binder, interface orParcelFileDescriptoris&mut [T; N], not&mut [Option<T>; N]. Service implementations and callers of such methods stop compiling until updated, and#[rsbinder::interface]refuses the old spellings and names the new one. The bytes on the wire do not change.rsbinder-aidl: a union variant is its field's name with only the first letter uppercased (CHANGELOG Fixed), as AOSP names it:nullable_ifaceisNullable_iface(wasNullableIface) andURLisURL(wasUrl). A single lowercase word (ns→Ns) or a lowerCamel name (myField→MyField) keeps the variant it had. Code naming any other variant stops compiling. The wire carries the tag number only, so it does not change.rsbinder-aidl: one statement may hold at most 12<that are followed by a name and not closed by a>(CHANGELOG Fixed); 0.11.0 allowed 256. A comparison counts, so a constant such asA < B || C < D || …with 13 or more such comparisons is now rejected; split it across several constants (each;resets the count).rsbinder-aidlchecks argument directions as AOSPaidldoes (GetArgumentAspect,AidlArgument::CheckValid); 0.11.0 generated code for all of the following. Refused:outorinouton anIBinder, an interface or an enum (inonly,@nullableor not) and on a@JavaOnlyImmutableparcelable or union;out ParcelFileDescriptor(inorinoutonly); and an omitted direction on a type that takes more thanin— any array (fixed-size too, whatever the element),List, a parcelable or union (generic too) andParcelFileDescriptor. Writeinwhere the direction was omitted; the generated Rust does not change. For anoutbinder, interface or enum, return it, or use an array (out IBinder[]is&mut Vec<Option<SIBinder>>, and an element leftNonegoes back as a null binder); for anoutfd, useinout. Anoutorinoutprimitive orStringstays refused, now asSemanticError::InvalidDirection.#[rsbinder::interface]and.aidlrefuse AOSP's reserved method signatures —asBinder(),getInterfaceHash()andgetInterfaceVersion()with no argument, andgetTransactionName(int)— as AOSPaidldoes (AidlInterface::CheckValid). Rename such a method; its transaction code, set by declaration order, does not change.rsbinder-aidl: an enum reference in a constant expression takes the type of the enumerator's value, not the enum's@Backingwidth, as AOSPAidlConstantReference::evaluatecopies the referenced value's type (@Backingonly bounds the value,AidlEnumerator::CheckValid). 0.11.0 widened to@Backing, so in along-backed enumBIT = 1thenBIT << 40, and an implicit member after2147483647, folded; both are nowintoverflows. Write the literal at the width you need (BIT = 1L). A unary operator applies at that type too: withA = true,-E.Aistrueand~E.Ais an error; withA = 128u8,-E.Aoverflowsbyte.rsbinder-aidlbinds a dotted name as AOSPResolveNamedoes, for constant references and type names alike: the first segment is looked up in the enclosing types (innermost first), then the imports, then the document's own top-level types, and the rest of the name must exist inside what it found; a first segment found nowhere makes the name fully qualified. A type of the same package declared in another file without an import (an rsbinder extension; AOSP rejects it) is tried only after that, so it never outranks an import or a fully-qualified name. In a package-less file an import now outranks a package-less type of the same simple name. SoQ.Xlooks forXin the typeQnames and nowhere else. 0.11.0 also tried shorter suffixes of a wrong qualifier (wrong.p.I.Xfoldedp.I.X), kept searching outer scopes after an inner type took the first segment (X.Y.Z.Cbeside a nestedXfolded the top-level one), accepted a qualifier that repeats a type's name (X.X.C,E.E.A, a field of typeX.X), let an unqualifiedXfind a constant inside a nested type also namedX, and could foldP.Xto a nested type's constant instead ofP's. These names are now errors andP.Xfolds toP's constant. Name a shadowed outer type fully qualified (p.X.Y).rsbinder-aidllooks an unqualified constant up in the current type only, as AOSPAidlConstantReference::Resolvedoes. 0.11.0 also searched enclosing interfaces, sointerface I { const int K = 5; enum E { A = K } }built with rsbinder but fails in AOSPaidl("Can't find K in E"). Qualify the reference:A = I.K. This includes a field default of enum type:E e = B;is an error (AOSP "Can't find B in P"); writeE.B.rsbinder-aidland#[rsbinder::interface]reserve the__Rsbname prefix (CHANGELOG Fixed). An.aidlinterface, parcelable, enum or union whose name starts with it, an interface trait so named, and a signature path whose first segment starts with it are now rejected; AOSP'saidlaccepts such names. Rename the type, or reach it in a signature through a path such ascrate::….rsbinder-aidl: an AIDL keyword is a whole word and never a name, as AOSP's lexer (aidl_language_l.ll) tokenizes it. A field, argument or enumerator namedin,out,inout,package,import,const,interface,parcelable,enum,unionoronewayis now a syntax error (int in;used to pass when;or,followed the name); rename it. Text such asinterfaceIFoo {,packagep;oroneway oneway interfaceis rejected too. AOSP'saidlrejects all of these, so an.aidlshared with an Android build is unaffected.
rsbinder-macros
#[rsbinder::interface]and the derives now refuse shapes.aidlcannot express, so moving an interface to.aidlstays a no-op. The diagnostic names the.aidlform to use instead. Refused: directions and nullability AIDL lacks (out String,Option<i32>, a bareoutbinder or fd);inspellings.aidlnever renders (&String,&Vec<T>,&i32, a by-valueStringor binder); array-elementOptions spelled the other way (&mut Vec<Option<Cfg>>,Option<&[String]>) andi8in an array element; scalars AIDL has no type for (u32,u64,usize,u128,char); and other shapes such asBinderResult<T, E>,()or&mut [T]arguments, duplicate names, receiver attributes, an emptydescriptor, and on#[derive(Parcelable)]a bare binder or fd field.#[rsbinder::interface]refuses anoutor#[inout]binder object and anoutParcelFileDescriptor,Optionor not, as AOSPaidldoes (GetArgumentAspect:IBinderand an interface goinonly, a fdinorinout).&mut Option<rsbinder::SIBinder>,&mut Option<Strong<dyn IFoo>>and&mut Option<ParcelFileDescriptor>compiled in 0.11.0 and had no Android.aidlcounterpart. Return the object instead, pass the fd#[inout], or use an array, which takes every direction (out:&mut Vec<Option<_>>,#[inout]:&mut Vec<_>).#[rsbinder::interface]refuses two interfaces in one module whose names share a stem —IFooandFooboth nameBnFoo/BpFoo(AOSPClassNamedrops a leadingIbefore an uppercase letter). This failed with E0659 only onceBnFooorBpFoowas used; it now fails at the second declaration with E0428 on__rsbinder_one_interface_per_stem_Foo. Move one of the interfaces to another module or rename it.
rsbinder-tools
rsb_hubchecks every directory above its configuration, not only the configuration itself (CHANGELOG Changed). A group- or world-writable (sticky or not) or foreign-owned directory anywhere on the path refuses the load: one kept under/tmpno longer loads, nor does~/rsbinder/hub.dwhen~/rsbinderwas made0775under umask002. For an unprivilegedrsb_hub, move it under$XDG_RUNTIME_DIRor$HOME; a rootrsb_hubneeds a root-owned path such as/etc/rsbinder/hub.d. Either waychmod g-w,o-wthe directories on the way.rsbinder_tools::config::check_pathis removed: the check now runs on held descriptors, and a path-based probe would report a different inode than the one read.rsb_hubrefuses configurations 0.11.0 loaded: it exits 1 at start, and a SIGHUP reload keeps the policy in force. A[[service]]name must beinterface/instancewith both halves non-empty, withinaddService's name rule (at most 127 bytes of[A-Za-z0-9._/-]); a bare interface name, which 0.11.0 read as<name>/default, is refused. Asystemdunit must not be empty, anexecprogram must be an absolute path, andconnection.portmust be 1–65535 (ConfigError::Tomlotherwise). Anexecprogram must exist and pass the check the configuration itself gets (CHANGELOG Security) — the program and every directory on the way to it from/owned by root orrsb_hub's uid and not group- or world-writable — so a program under/tmpor under a group-writable directory is refused.--configmust name a regular file or a directory; in a directory, a*.tomlentry that cannot be opened (a dangling symlink) fails the load, where 0.11.0 skipped it, and a file whose name starts with.(an editor lock or backup file) is no longer read.rsbinder_tools::config::ConfigErrorgainedEmptyUnit,RelativeExec,UntrustedExec,ExecIoandBadServiceName; an exhaustivematchneeds the new arms.config::InstanceName::parsereturnsOption<InstanceName>:Nonewithout a/or with an empty half.rsb_hubanswers differently in three cases.getDeclaredInstancesreturnsSecuritywhen the interface has declared instances and the caller mayfindnone of them, as AOSPServiceManager.cppdoes; 0.11.0 returned an empty list, sorsb_service instancesexits 2, not 1.addServicesucceeds once the service is registered even if anonRegistrationcallback fails; 0.11.0 returned that callback's error.rsb_hubexits 1 when its signal, death-notification or client-callback thread cannot start, orsigwaitfails, where 0.11.0 kept serving without that thread.rsbinder_tools::nss::gids_for_uidreturnsOption<BTreeSet<u32>>:Noneis a failed lookup, not an unknown uid (that isSomeof an empty set).nss::GroupCacheandconfig::Enforcerhold a boxed resolver and are no longerUnwindSafe/RefUnwindSafe; wrap them inAssertUnwindSafewherecatch_unwindneeds it.rsb_serviceexits 2, not 1, when the device has no service manager: 0.11.0checkanddumpreported every name as not registered there.rsb_devicerefuses a device name over 255 bytes or naming a binderfs entry (binder-control,features,binder_logs) before it mounts anything; 0.11.0 applied--group/--modeto that entry.