From c79559558942f4de95504b06c8f9c84028858475 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Thu, 13 Aug 2026 19:16:47 +0530 Subject: [PATCH 1/3] fix(x11): verify display access inside proot --- app/build.gradle.kts | 13 ++ .../assets/runtime/arm64-v8a/runtime_probe | Bin 5456 -> 7648 bytes .../assets/runtime/armeabi-v7a/runtime_probe | Bin 3780 -> 5788 bytes .../main/assets/runtime/x86_64/runtime_probe | Bin 5304 -> 7576 bytes app/src/main/cpp/runtime_probe.c | 93 ++++++++++ .../udroid/runtime/NativeProbeInstaller.kt | 2 +- .../runtime/RuntimeSupervisorService.kt | 51 +++++- .../udroid/x11/GuestX11TransportProbe.kt | 165 ++++++++++++++++++ .../udroid/x11/GuestX11TransportProbeTest.kt | 71 ++++++++ 9 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 44fba17..3dcbf2e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,11 @@ val hasUpdateSigning = updateSigningKeyAlias, updateSigningKeyPassword, ).all { !it.isNullOrBlank() } +val x11GuestProbeFault = + providers.gradleProperty("udroidX11GuestProbeFault").orNull ?: "none" +require(x11GuestProbeFault in setOf("none", "deny")) { + "udroidX11GuestProbeFault must be none or deny" +} kotlin { compilerOptions { @@ -48,6 +53,7 @@ android { "UPDATE_RELEASES_API", "\"https://api.github.com/repos/RandomCoderOrg/udroid-app/releases?per_page=20\"", ) + buildConfigField("String", "X11_GUEST_PROBE_FAULT", "\"none\"") ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") @@ -66,6 +72,13 @@ android { } buildTypes { + debug { + buildConfigField( + "String", + "X11_GUEST_PROBE_FAULT", + "\"$x11GuestProbeFault\"", + ) + } release { isMinifyEnabled = true isShrinkResources = true diff --git a/app/src/main/assets/runtime/arm64-v8a/runtime_probe b/app/src/main/assets/runtime/arm64-v8a/runtime_probe index f5e847e66c43225001127352aa83bffc3ce61562..49d377dc407b824c26df60715ff6b726b8a0188a 100755 GIT binary patch literal 7648 zcmdT}eQ;FO6~DVL;0hsK%z|kZcUj7qL;p=oy}(Vk*wLgx9q-P zB7THm)hUi;AtJ2>5p7G-(Mc`pXgf8g&d7|NsUo0GM`wXLO{e`M*uapf%l3EPy9aJo z)`^b)^k(j!dw%ERo_p@Q=Y8a%TUOudcDV#MO}r|I3;n=BLQ>FDAqOBKv0Ti=?_4oQ zOb1_LV~#xJ5J;WY?w(9N64yYAe2Fy;A{qO7h2cj zMerR(@I6KFUlqY$D1yII1pi|Ze5432g!?P~xo`sBhA%HVwdgFWc5d-kn{&=b1;%v! z(KR)xg~N$>Ycw1;;+6=9qur5k#L~NMCZ)HGwR zttT|08zPyC8`gT!rd!D*&S5&CverdYkIzJZun zf;ic>?Q{m3EGS(6cNM|+7UF9h`(9bc+0Vb_{bRq>cfmhpp4&l*yMc4fCE2?QE=!?p z@}Yv$J(62i!Q~Qc(Wru}^>|FdFJdR4XB3?89k%!>M~ULRSkd<=IN#z(>{alYihhNH z%Ug;q4JbI(I&PH;E^pnos8+$h#mb<|6`Zei5{DFAt$&RQuGWdQ3SO@GU#H+c1#eMs zekLJtyMoVE^pgsHse*3`>Kk>#3SJ-VzWVBLTbG`;!pW4`ss|SZ(^kalN<-8hF=FZV zNQWL?ABiXQ7-aV82wuN1wkR0WEuc_am<~QLLrlq|JUw5{U%MVjS*?1+!u+kKX@%o4 zXI6x&Z|KsEsE&vdU5kR9reRv95s!vD5&yzOB8I>koD&;2jevbtW1`FGFwA?6sfFxR zg0B{_oyl<8GLuO=YhM=iR8yDI??K(M-J#rdCE?!LriEBICbZ0`nFx1A?lDtX`XnrF zI2fi{ycqkvIHnstQ(OI%Hf*h)>Wrt;lwo*|>#;y(+;F0;ntW++Vz8kXN(`#Cb8S)G zrqNWB6E`+S67g6#8L`@nT%~u~6+J*2fp|J#nPwni8f_Gxos_BNgDQLvW|(rq<(X4! zEq_UXCG{t#@AN!1V^`_Vy?ZX%?VF+bN=p5ZmWlTdA36HLhaY|N@k+`T#W;#?-6Diq z(*cQRzN;OXCwdQEfu7Yw#s&487HCKEn(MvXCT$@1XOXEyTC32JIMcJLVWR4ehKWj- z=NlcAcTDpS)~{`txZNfC!Tcvc>w*wVK* zOn4BJ$5s9>+@za5xrQC_GS61uH1S^5=J1(->!zclns3m`=U*OQbMruM)5!ie;EU$Z zZ_%*Miw7p|6ccB{GY3xJRSN&J267GVjK5TDIHkD;-zgEzzn>nOpjaZKx!SB&J{ZJ$ zhOv#u^7(D^_b$#5HV;^!L@cA2Xx%~4+vNFC!^8=Ug)21iZOHc?3JtK$Z1AJ7_iSF8 z5w77C*^7Etgl37lEMmO`GXIe4*{Sf2A>xL4TM!EpozL)g8Hyl_^!q~Q2%kVqy6(-iFNv_u?FO;ym}CH9Pu<@ zO+Ul9+_ovN-mBUiuALvCn3f2}w|IzB#v1y!V^4n!N^3uWYlzn037*zZ*V_nk zP@kyE*tWw9?OZH_oaSjGJFT~gY{u8zfm#uoIDOaZfivOnqn4~1Kn=-IEwOFKugY8r z{W|E6W6nmL1ApeC*GMkV#x}h%Pz!8!Yr}-seOV@eI!HO=Zp+p~7Ib7be-HS2g*U*r zDtrojpTchhzfa-s13#kh4}w3g@O|Ljm)o(}^F0DSsPI1pU$5{_fNxd!?cn-(^drBEJLT{$aOMXPxzpO2si5-!93Y zl|1UW-SX%2;Q-&z@6er&qyHC*JlL&p`S$fygRd`^sA)6tgBL5k2JeIPWwR!Z$9zXn zlT2Wc(i0J9)S6CG;bSBuI&k56z6wn+rH=kcVKXmx9(I(sT zxnPQVX*WKntjCl-FVPudp3f1}fMag2k>)Rq|6yRK6?;CXO!-}d=jZsPTl+nQWIR9D z0j69BSidm;Is_CG+jE^`+AR$W=jZ(4y}`DxmwH_1n5uP-zWZ?eZ2vkiis>3@pOgia z>Ek@Hl(W5LMF<13=lF~Le}wf5^S`^m9?$1?V`@8RW6;9+X>RIVr}?`j{~s$M>ngh_ zoWG{P{#<>OAH90XxzfDKn1;VWr@1*z{C_Y0KU0ge=lcoA&wlt^uswf=-8PrJ3lXq? z&;PJJBP6FO*q-lWL(={rD@s~u{{(Qt*uMKIN0I+8_gH~JK{~_;)YbXV{U1->c=`M| pUcb11c*mSH|1P{NQ0tfW1=D|F|A(NI;CQ%g0Q3W+^sVuuuRjDj$rTl2sI6oTX^oONNsq?^f)FxWuR^6)oK`o3}wd?J7 zX70hml&Atp1(|2+Jt&i|FjpV~YPbOW5R#e$LZ?aGX0Ol@RWG@~A3H!9N2>Fl4Rm*vk<(Z< zvTrFPr%Bn!et#MHLCCkDxv2|+jpB5dksm7~f2NE)Sw?=Mj9ds$**^tu|LgGcp%KOI zqMhy$FIt@apn0dQNJw+Kj>6$&A{q-P^n@wG;n>MYIAUriEheoECb00*kPH~MH!?|0 zOCgb#PV0uC)WM7)jy%zRh?sbixbAP+6{>Or@Hy32M&9q1KWY0fy|q5hvOL#^q`iXp zcQ_{FB`NnnPIJXH>5$8Cp6s*>w=_0oo4UUPNNb@Nhr*YEKLMTs&I2cb zzX$#Z_*dX-z)-sodEoLPAs$fB0r4JGzSbj@Q#+Jx)qX*DT|L@TNQge5=y<53Ce&5E z-FLxr9*cu~=%8&x+*;CuNOlB`WPDeWtRJlvGR{Nipo!K3SufCv-GJB)XFbyu7pl&C z&1!!`R}JLvzW3n=RvsEdH|&3=kbO<6uX=koS%0VMyWa0@nyLQ2@4`LjYt>UqwLjHY zBi?;);p%U%z5n3{R9g=1GREy(D-ekvJfJRY7ZdO7K+CCO3ZZKs+o>*;RORjBxSB1# zDW)1RjZHL_e-MVZsk6}ZP-mf0@&4LhDgKswUi>ttP8>LcVfY)=g_oKueEH3y5Po9#IMR0pBGbpuQ)!8aqX@M6>vt> zQY~Ba#nv&51#>z%ePh?uO;t4Kl!=~2#D5m@kg)r`zzp~X;-Sa8o<^)`zo;8l?i9UL zH_c4|dha8hh3n`GB~;L1a~y3Mal=5&VRNojniTmRn9HqmAE~v?Baq|Xn#_BSUc6i> z+Q!bk_zu<9_@|=p{gu~m2xb1jO~iZp%I|K}pPuMhhX3@X`=`cY`5W}EXe{LGtq^UC zhr|@sxT4n0eF<^#e%e$q@ok)CwSV4uFzY@2^tnyNp0i!Wt}{D7U4JfzaoTmA_IP$R+<|x2p2fSGqIcGXvHO*~FH#>%*rKKJH?pme zZ)xu=_&ocj8nC`-PPkdR!_Wn6oy9*1zTLq;1wQKFd%!>E;J*O=hYtRW;4eA&XTX2t z;G^I@`>k56d;{PE4*sj)+a0_PKI-7p;GeU3+VVu$XBII}-w#APY_Ps7sr&a*$smNC zmyrJ-p*|7coip+>vy31=X=#PvyQyCC1sM;=o<;Xb^vO$)@74xc7}l6YDS02!8AQZ+ z`L67fhMR4*b>32!@7N02;X83!+TFZmk1sC^T9Wn6O8%DQe+=md!`5u8hGHU)`AiHKZ#%>j=SDc1XD?L(poZN_oA7M2*$_sjF}ce zZ74jDj*MvGp*XlfeKdGNOJ@>>9tMk#KrNk;zVKLQV8<96v4umC%#dZ6Ho~b$Jf6@8 zZwI$bBO{5J2xf+m){I19Ys4dF1S)!d67&@Wp*i7oSnmIF&Lx*Av(~*xYm@!wWrq>> z(K_ONz(V@opf$|?ysjC$WkSk9UEXiJzi6}KMH}qT>zI+hC3s)49ph8*r!~(!uY1M@ z=!rPLyZ#rTTXFdFzG8Ie=lZRnc{rOyCJSiP{{G6ZX z58o%2f4j8fbBWP;F46yuZvWRIqnaL;{;l$$V!S5h4D8Q*2p#h0`pf+u!S?R?KkxGI zmI)YF*-@f9Kjo&z=PLi-#lIWPb79iooj>UEUpvp_2V+k9G5EM6@y9^Q&F^RJ_a^Dz zPsM@cw64uR$KiF!{@wE4=&rH-8f;j5y2np?O2Pho@1&$ZuOANJ_NR0tWBurSbc(xWsTGEGiwo|@v{YIv=xmD$p@b=hq_OJE?9A)#qqFZl zX5N;rh?b9H4Xo2dh-e`5B$CufbS3;DT1cafAc0t86A6Jh9}Zo%71vrnf`sSq_ujlR zeWKZtcsFOy{hf2}x#!+{?w$MY*FL@VGm4^!U)p3qi1uG3vL1Z!1Naw`j`b3g^PpQP zJ+cHM>U>J00jV7_QXA+&V4vuML>V5xy{M(?1*?$z*Kcuuf zh?snO7??zP(}x+yV9pTa@fQ6^ak^-!lOtoAK9bk;U1~uj4E?D;Qc%R-s;4 zw=oO$zRTJxui9|cl^@&qz8wS_GCsdx31pcMz=;{NUx){N08V^m0DmHYv7~1T?XN$B zy!P7yczXbUDS*`gE(Y+O0lY7O9|+)Y2XHxn4+ij$0{E!_e!2y(K>ctF=J=V-=kli@ zgPubxYy-DsmvL8xQh9AGo6>a6LOr`Dlge1?9v2kUaSaJufMlmp z*d;})kexK8s9L+nrgjqMRb9-2rdvB@T(wLS$y?OM^-Lbptd)0#kY)2mc2^1(mNu!1 zp3#kBUR6!>rxpsjAw{TFi;h_{OiQwcuB%yytO87o0$mhUxxJuSsua`=`mu7xlqHku zWY(M(j$ycH$kyvOZz0I#iBRmdA{T;xzsV0ac`p)<@+W|MR^lAOLBhFtvB{4%`COAP zG~|GV`|@EU(w1~Q$?sIGJ38!k=doDc4oA^nuE?=puSCOa)W(2 z)q)geYl?k$Mi%F>xsK=NZ$ix!tT8oXq5m<%uu@vi>lLA@w@s;fRz*blslmQULpLl# z*RrWe#J?t=&mpiaUhZgy9&@uMCsX<^-MC#}T*zKBo?FE3Os0yKVVdr&{jaDOJ9P#1 zR_p-R9X7_43HRPIt--+2p=D%^d}=att5Lwv&ys1nCqq|5Ls;*joT^VRZuE;%c6EwV$jF!YIkg=@ls^;w8ZXe3`WZhC0af{5GyxAvGQ>xy1Amrw)*O5 z`Eey$vcu7t$Gf4Es9mRiry?hgVV@rb#Ro*5;^8LA`o+$O5_LwZu&Z`SxzWWoQmTYv zrOFo-nXe$Iw?BgOBVKN_#Y>ehKsE;)N1Wl=nc85Y+-OfA4zv|1A8(grCBFZeaAKQX z?NIKmv`e%Rhkqq`OYJSkSq0tPR`h)xXyK-zH&(;P0~o84w5x512ci9JdpCVPgz=To z-mY#z?2Hx1Wh6lzWYk^LlwX3Mtig6~tP18z;&xq0fTux-N#sD|D%f_pZ9TtX^P$F2 zvwc^3rV&oeH1hD@3Ev}-=h2@ovZ>NmZuDagIVZI5G41;6BJ&~W5o1SXOSz{#QHovx z-OE|TfN?AryS^k{sw}~r4nQ6vMm>bKxKo}`fS1GP8q`;VIwV>BEynN&`r>+>8H4?9 z{7Yc>A&~2zJZ@J_*tf%PM+`ZF_&Oj@(+B$T#+|I=ei=!AtK5jDXKJ$7t}i`Ms;f2;x@Nz7%Xj-R1b-O)!I_Y5zqfAGWC)}!)hW5ua$$44)> ztKS>ByYkj0cKsKhva1I!tydOeFIAUf4*j?kb6@QbA}6}{&NQO-Oyl{`yZyfAIFTi= zWxsF2hW)(^*`M9|0jzVfehT<^*cT6@9es8-$NB`MjjkcW_$8@ug4%Bt@@hmXMk8Kh8G3&_# zjSkozVm)S8(@MEPTaNMmW?S45Ph5zx;;J(L80`|AGrQ`xN1Fk(;heF}SD{}C+YPYo zUE%GaF7(TN^h4;n&^Px{fBJ?Ra;%QMHa|n3*gISY+znTS<(4|GB3E=o@2iAkYK?u_ zu;HBk)p1^7{J4rlI+A-Uv_0)On`l#lO@DMpV|Lw+>XUstsywIY&&HV%LVRtAl`(z; z>uj=5j9t(kc^csy+(8*^6vULq`NXp+jkf-Ld=`54GX>s@F>=0FAHAWrPp@zI(swwl| ztX+NT*~YOzkzYag!UMJbQ-@rgk9z~SD}W_{PyglY_HzOJY5*S%;3EP2(*S-Vfb9T& zFo5?5@K*xZ4B%VmQ9^yV9f@u8U8 zBg6P$v}AbPz(3r_)nR-_@F8PZ!|l*h44ZjW3Tob~j1`NXgN$Vrw6Q5m1S*;9{7|Y5KTpkeSr7GF+U1cPldn z3nQ1YGEnjVITQ`HoHyo1>l>Kw!e3vU@zU9}Yke#7?U-jQUk5Rd=#M!?+6yOaOJB5O zj(r)#{GlCliiF>N0douGq|bwxZ>%%tNHNq~{Q(NjYx#TAj=4(W+ko%SkMHKDjQ*J8 zB<8qZ{PErf0qziP4BT5J?yXjT++(x{nRkP?zLDW`*DZd(>w)NRGl+YUG>ba@aXx5A ze?JG&Cq8i{ij7H{@;hznz=Zy3$Gz+BJ2;<%Prvy6ZiK+MbNAnJZ#{fF_RVq-L|^R6 zeXE3m@o}D6{P=jr&>!vi4q@W}@qIw#+L3=0>wrE@%2;T3wO4ij`~Cg|h_U%4 M4ZC0a0Nl6xH+z%rGXMYp delta 1751 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 Gqy7bg;M73? diff --git a/app/src/main/cpp/runtime_probe.c b/app/src/main/cpp/runtime_probe.c index 4c45310..2624ba2 100644 --- a/app/src/main/cpp/runtime_probe.c +++ b/app/src/main/cpp/runtime_probe.c @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -23,7 +26,97 @@ static int64_t monotonic_ms(void) { return ((int64_t)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000); } +static int read_exactly(int fd, uint8_t *buffer, size_t count) { + size_t offset = 0; + while (offset < count) { + ssize_t received = read(fd, buffer + offset, count - offset); + if (received < 0 && errno == EINTR) { + continue; + } + if (received <= 0) { + return -1; + } + offset += (size_t)received; + } + return 0; +} + +static int probe_x11_socket(const char *path, int force_denied) { + static const uint8_t setup_request[] = { + 0x6c, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + uint8_t setup_header[8]; + struct sockaddr_un address; + struct timeval timeout = {.tv_sec = 1, .tv_usec = 0}; + + if (force_denied) { + printf("{\"event\":\"x11_guest_probe\",\"status\":\"connect_failed\"," + "\"errno\":%d,\"detail\":\"Permission denied (injected)\"}\n", + EACCES); + return 20; + } + if (strlen(path) >= sizeof(address.sun_path)) { + printf("{\"event\":\"x11_guest_probe\",\"status\":\"invalid_path\"," + "\"errno\":%d,\"detail\":\"Socket path is too long\"}\n", + ENAMETOOLONG); + return 21; + } + + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + printf("{\"event\":\"x11_guest_probe\",\"status\":\"socket_failed\"," + "\"errno\":%d,\"detail\":\"%s\"}\n", errno, strerror(errno)); + return 22; + } + (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + memcpy(address.sun_path, path, strlen(path) + 1); + if (connect(fd, (struct sockaddr *)&address, sizeof(address)) != 0) { + int saved_errno = errno; + printf("{\"event\":\"x11_guest_probe\",\"status\":\"connect_failed\"," + "\"errno\":%d,\"detail\":\"%s\"}\n", + saved_errno, strerror(saved_errno)); + close(fd); + return 23; + } + if (write(fd, setup_request, sizeof(setup_request)) != (ssize_t)sizeof(setup_request) || + read_exactly(fd, setup_header, sizeof(setup_header)) != 0) { + int saved_errno = errno; + printf("{\"event\":\"x11_guest_probe\",\"status\":\"handshake_failed\"," + "\"errno\":%d,\"detail\":\"%s\"}\n", + saved_errno, saved_errno == 0 ? "Connection closed" : strerror(saved_errno)); + close(fd); + return 24; + } + close(fd); + + unsigned int protocol_major = + (unsigned int)setup_header[2] | ((unsigned int)setup_header[3] << 8U); + unsigned int protocol_minor = + (unsigned int)setup_header[4] | ((unsigned int)setup_header[5] << 8U); + if (setup_header[0] != 1) { + printf("{\"event\":\"x11_guest_probe\",\"status\":\"rejected\"," + "\"setup_status\":%u,\"protocol_major\":%u,\"protocol_minor\":%u}\n", + (unsigned int)setup_header[0], protocol_major, protocol_minor); + return 25; + } + printf("{\"event\":\"x11_guest_probe\",\"status\":\"ready\"," + "\"protocol_major\":%u,\"protocol_minor\":%u}\n", + protocol_major, protocol_minor); + return 0; +} + int main(int argc, char **argv) { + if (argc >= 3 && + (strcmp(argv[1], "--x11") == 0 || strcmp(argv[1], "--x11-deny") == 0)) { + setvbuf(stdout, NULL, _IOLBF, 0); + return probe_x11_socket(argv[2], strcmp(argv[1], "--x11-deny") == 0); + } + const char *boot_id = argc > 1 ? argv[1] : "unknown"; const pid_t original_parent = getppid(); diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt b/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt index 2b00b6c..a759b31 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt @@ -6,7 +6,7 @@ import java.io.File import java.io.FileOutputStream object NativeProbeInstaller { - private const val PROBE_VERSION = "1" + private const val PROBE_VERSION = "2" private val supportedAbis = setOf("arm64-v8a", "armeabi-v7a", "x86_64") fun install(context: Context): File { diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index e8567d5..c531ba1 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -38,6 +38,8 @@ import org.randomcoder.udroid.audio.AudioSessionSnapshot import org.randomcoder.udroid.install.ProotRuntimeInstaller import org.randomcoder.udroid.linuxapps.LinuxApplication import org.randomcoder.udroid.x11.X11ServerController +import org.randomcoder.udroid.x11.GuestX11TransportProbe +import org.randomcoder.udroid.x11.GuestX11TransportResult import java.io.BufferedReader import java.io.File import java.io.InputStreamReader @@ -57,6 +59,7 @@ class RuntimeSupervisorService : Service() { private val pendingDesktopRestart = AtomicReference(null) private val attachedViews = CopyOnWriteArraySet() private val applicationProcesses = ConcurrentHashMap() + private val guestX11TransportProbe by lazy { GuestX11TransportProbe(this) } private val applicationExecutor = Executors.newCachedThreadPool() private val x11Controller by lazy { X11ServerController(this, app.journal) } private val audioConfigurationStore by lazy { AudioConfigurationStore(this) } @@ -586,10 +589,56 @@ class RuntimeSupervisorService : Service() { runCatching { if (desktopLaunchToken.get() != launchToken) return@runCatching null val rootfs = InstalledRootfsResolver.resolve(this, request.rootfsName) + val prootRuntime = ProotRuntimeInstaller.install(this) + when ( + val probe = + guestX11TransportProbe.query( + runtime = prootRuntime, + rootfs = rootfs, + socketDirectory = socketDirectory, + ) + ) { + is GuestX11TransportResult.Ready -> + app.journal.append( + component = "x11", + severity = "info", + event = "guest_transport_ready", + message = + "PRoot guest completed X11 protocol " + + "${probe.protocolMajor}.${probe.protocolMinor} setup", + bootId = runtime.bootId, + fields = + mapOf( + "rootfs" to request.rootfsName, + "display" to DISPLAY_NUMBER, + "transport" to "private_bind", + ), + ) + + is GuestX11TransportResult.Failed -> { + app.journal.append( + component = "x11", + severity = "error", + event = "guest_transport_failed", + message = probe.userMessage, + bootId = runtime.bootId, + fields = + mapOf( + "rootfs" to request.rootfsName, + "display" to DISPLAY_NUMBER, + "transport" to "private_bind", + "stage" to probe.stage, + "errno" to probe.errno, + ), + ) + error(probe.userMessage) + } + } + if (desktopLaunchToken.get() != launchToken) return@runCatching null val launch = ProotDesktopLaunchBuilder.create( context = this, - runtime = ProotRuntimeInstaller.install(this), + runtime = prootRuntime, rootfs = rootfs, x11SocketDirectory = socketDirectory, environment = request.environment, diff --git a/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt b/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt new file mode 100644 index 0000000..d15cd09 --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt @@ -0,0 +1,165 @@ +package org.randomcoder.udroid.x11 + +import android.content.Context +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.randomcoder.udroid.BuildConfig +import org.randomcoder.udroid.install.ProotRuntime +import org.randomcoder.udroid.runtime.ANDROID_PROOT_BIND_MOUNTS +import org.randomcoder.udroid.runtime.AndroidExecutableCommand +import org.randomcoder.udroid.runtime.NativeProbeInstaller +import org.randomcoder.udroid.runtime.ProotPathContract +import org.randomcoder.udroid.runtime.ProotTerminalLaunchBuilder +import java.io.File +import java.util.concurrent.TimeUnit + +private const val EACCES = 13 + +internal sealed interface GuestX11TransportResult { + data class Ready( + val protocolMajor: Int, + val protocolMinor: Int, + ) : GuestX11TransportResult + + data class Failed( + val stage: String, + val errno: Int?, + val detail: String, + ) : GuestX11TransportResult { + val userMessage: String + get() = + if (errno == EACCES) { + "X11 guest transport denied: $detail" + } else { + "X11 guest transport failed: $detail" + } + } +} + +/** Verifies the display socket through the same PRoot alias used by Linux clients. */ +internal class GuestX11TransportProbe(private val context: Context) { + fun query( + runtime: ProotRuntime, + rootfs: File, + socketDirectory: File, + ): GuestX11TransportResult { + val nativeProbe = NativeProbeInstaller.install(context) + val arguments = + GuestX11ProbeCommand.buildArguments( + prootPath = runtime.executable.absolutePath, + rootfsPath = ProotPathContract.rootfsPath(context, rootfs), + socketDirectory = socketDirectory.absolutePath, + nativeProbe = nativeProbe.absolutePath, + forceDenied = BuildConfig.X11_GUEST_PROBE_FAULT == "deny", + ) + val temporaryDirectory = + File(context.cacheDir, "proot").apply { + check(mkdirs() || isDirectory) { "Could not prepare PRoot temporary storage" } + } + val environment = + ProotTerminalLaunchBuilder + .buildEnvironment( + androidHome = context.filesDir.absolutePath, + loaderPath = runtime.loader.absolutePath, + temporaryDirectory = temporaryDirectory.absolutePath, + ).associate { value -> + val separator = value.indexOf('=') + value.substring(0, separator) to value.substring(separator + 1) + } + val command = + AndroidExecutableCommand.create( + runtime.executable, + *arguments.drop(1).toTypedArray(), + ) + val process = + ProcessBuilder(command) + .directory(context.filesDir) + .redirectErrorStream(true) + .apply { + environment().clear() + environment().putAll(environment) + }.start() + if (!process.waitFor(PROBE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + process.destroyForcibly() + return GuestX11TransportResult.Failed( + stage = "timeout", + errno = null, + detail = "guest handshake timed out", + ) + } + val output = process.inputStream.bufferedReader().use { it.readText() } + return GuestX11ProbeOutput.parse(output) + } + + private companion object { + const val PROBE_TIMEOUT_SECONDS = 3L + } +} + +internal object GuestX11ProbeCommand { + const val GUEST_SOCKET = "/tmp/.X11-unix/X0" + const val GUEST_PROBE = "/tmp/.udroid-x11-probe" + + fun buildArguments( + prootPath: String, + rootfsPath: String, + socketDirectory: String, + nativeProbe: String, + forceDenied: Boolean, + androidBindMounts: List = ANDROID_PROOT_BIND_MOUNTS, + systemLinkerPath: String = AndroidExecutableCommand.systemLinkerPath(), + ): List = + buildList { + add(prootPath) + add("--link2symlink") + add("--kill-on-exit") + add("--root-id") + add("--rootfs=$rootfsPath") + androidBindMounts.forEach { path -> + add("-b") + add(path) + } + add("-b") + add("$socketDirectory:/tmp/.X11-unix") + add("-b") + add("$nativeProbe:$GUEST_PROBE") + add("--cwd=/") + add(systemLinkerPath) + add(GUEST_PROBE) + add(if (forceDenied) "--x11-deny" else "--x11") + add(GUEST_SOCKET) + } +} + +internal object GuestX11ProbeOutput { + fun parse(output: String): GuestX11TransportResult { + val record = + output + .lineSequence() + .map(String::trim) + .filter(String::isNotEmpty) + .mapNotNull { line -> + runCatching { Json.parseToJsonElement(line).jsonObject }.getOrNull() + }.firstOrNull { value -> + value["event"]?.jsonPrimitive?.content == "x11_guest_probe" + } + ?: return GuestX11TransportResult.Failed( + stage = "probe_output", + errno = null, + detail = output.trim().takeIf(String::isNotEmpty) ?: "probe returned no result", + ) + val status = record["status"]?.jsonPrimitive?.content.orEmpty() + if (status == "ready") { + return GuestX11TransportResult.Ready( + protocolMajor = record["protocol_major"]?.jsonPrimitive?.content?.toIntOrNull() ?: 0, + protocolMinor = record["protocol_minor"]?.jsonPrimitive?.content?.toIntOrNull() ?: 0, + ) + } + return GuestX11TransportResult.Failed( + stage = status.ifBlank { "unknown" }, + errno = record["errno"]?.jsonPrimitive?.content?.toIntOrNull(), + detail = record["detail"]?.jsonPrimitive?.content ?: status.ifBlank { "unknown failure" }, + ) + } +} diff --git a/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt b/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt new file mode 100644 index 0000000..6a0fdb5 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt @@ -0,0 +1,71 @@ +package org.randomcoder.udroid.x11 + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class GuestX11TransportProbeTest { + @Test + fun `ready output preserves negotiated protocol`() { + val result = + GuestX11ProbeOutput.parse( + """{"event":"x11_guest_probe","status":"ready","protocol_major":11,"protocol_minor":0}""", + ) + + assertEquals(GuestX11TransportResult.Ready(11, 0), result) + } + + @Test + fun `permission denial becomes a transport-specific error`() { + val result = + GuestX11ProbeOutput.parse( + """{"event":"x11_guest_probe","status":"connect_failed","errno":13,"detail":"Permission denied"}""", + ) as GuestX11TransportResult.Failed + + assertEquals("connect_failed", result.stage) + assertEquals(13, result.errno) + assertEquals("X11 guest transport denied: Permission denied", result.userMessage) + } + + @Test + fun `probe uses the exact guest socket bind alias`() { + val arguments = + GuestX11ProbeCommand.buildArguments( + prootPath = "/data/proot", + rootfsPath = "/data/rootfs", + socketDirectory = "/data/x11/.X11-unix", + nativeProbe = "/data/runtime_probe", + forceDenied = false, + androidBindMounts = listOf("/system", "/proc"), + systemLinkerPath = "/system/bin/linker64", + ) + + assertTrue("/data/x11/.X11-unix:/tmp/.X11-unix" in arguments) + assertTrue("/data/runtime_probe:/tmp/.udroid-x11-probe" in arguments) + assertEquals( + listOf( + "/system/bin/linker64", + "/tmp/.udroid-x11-probe", + "--x11", + "/tmp/.X11-unix/X0", + ), + arguments.takeLast(4), + ) + } + + @Test + fun `fault build selects deterministic permission denial`() { + val arguments = + GuestX11ProbeCommand.buildArguments( + prootPath = "/data/proot", + rootfsPath = "/data/rootfs", + socketDirectory = "/data/x11/.X11-unix", + nativeProbe = "/data/runtime_probe", + forceDenied = true, + androidBindMounts = emptyList(), + systemLinkerPath = "/system/bin/linker64", + ) + + assertEquals("--x11-deny", arguments[arguments.lastIndex - 1]) + } +} From cf9c1ed33e49a62ebebaf74dd40fd6958592aae7 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Thu, 13 Aug 2026 19:47:28 +0530 Subject: [PATCH 2/3] fix(x11): place display socket inside active rootfs --- .../udroid/runtime/ProotApplicationLaunch.kt | 15 ++- .../udroid/runtime/ProotDesktopLaunch.kt | 15 ++- .../udroid/runtime/ProotTerminalLaunch.kt | 9 +- .../runtime/RuntimeSupervisorService.kt | 22 ++-- .../udroid/x11/GuestX11TransportProbe.kt | 12 +- .../udroid/x11/X11DisplayEndpoint.kt | 17 +++ .../udroid/x11/X11ServerController.kt | 105 ++++++++++++++---- .../udroid/x11/X11ServerService.kt | 7 +- .../runtime/DesktopEnvironmentScannerTest.kt | 2 + .../runtime/ProotApplicationLaunchTest.kt | 18 +++ .../udroid/runtime/ProotTerminalLaunchTest.kt | 18 +++ .../udroid/x11/GuestX11TransportProbeTest.kt | 3 + docs/LINUX_APPLICATION_LAUNCHER.md | 3 +- docs/X11_RUNTIME_ARCHITECTURE.md | 61 +++++++--- 14 files changed, 240 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/org/randomcoder/udroid/x11/X11DisplayEndpoint.kt diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt index 7aacf85..ee6c847 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt @@ -4,6 +4,7 @@ import android.content.Context import org.randomcoder.udroid.audio.AudioEndpoint import org.randomcoder.udroid.install.ProotRuntime import org.randomcoder.udroid.linuxapps.LinuxApplication +import org.randomcoder.udroid.x11.X11DisplayEndpoint import java.io.File data class ProotApplicationLaunch( @@ -17,12 +18,12 @@ object ProotApplicationLaunchBuilder { context: Context, runtime: ProotRuntime, rootfs: File, - x11SocketDirectory: File, + x11Endpoint: X11DisplayEndpoint, application: LinuxApplication, audioEndpoint: AudioEndpoint? = null, ): ProotApplicationLaunch { require(application.executable.isNotBlank()) { "Application executable is empty" } - require(x11SocketDirectory.isDirectory) { "The X11 socket directory is unavailable" } + require(x11Endpoint.socketDirectory.isDirectory) { "The X11 socket directory is unavailable" } val guestHome = if (File(rootfs, "root").isDirectory) "/root" else "/" val guestWorkingDirectory = application.workingDirectory @@ -32,7 +33,8 @@ object ProotApplicationLaunchBuilder { buildArguments( prootPath = runtime.executable.absolutePath, rootfsPath = ProotPathContract.rootfsPath(context, rootfs), - x11SocketDirectory = x11SocketDirectory.absolutePath, + x11SocketDirectory = x11Endpoint.socketDirectory.absolutePath, + bindX11Socket = x11Endpoint.requiresGuestBind, guestHome = guestHome, guestWorkingDirectory = guestWorkingDirectory, applicationArguments = @@ -69,6 +71,7 @@ object ProotApplicationLaunchBuilder { prootPath: String, rootfsPath: String, x11SocketDirectory: String, + bindX11Socket: Boolean = true, guestHome: String, guestWorkingDirectory: String, applicationArguments: List, @@ -82,8 +85,10 @@ object ProotApplicationLaunchBuilder { add("--root-id") add("--rootfs=$rootfsPath") addAndroidProotBindMounts() - add("-b") - add("$x11SocketDirectory:/tmp/.X11-unix") + if (bindX11Socket) { + add("-b") + add("$x11SocketDirectory:/tmp/.X11-unix") + } if (audioAuthDirectory != null) { add("-b") add("$audioAuthDirectory:${AudioEndpoint.GUEST_AUTH_DIRECTORY}") diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt index b480fd6..745c8a6 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt @@ -4,6 +4,7 @@ import android.content.Context import org.randomcoder.udroid.audio.AudioEndpoint import org.randomcoder.udroid.install.ProotRuntime import org.randomcoder.udroid.install.RootfsInstallationPipeline +import org.randomcoder.udroid.x11.X11DisplayEndpoint import java.io.File object ProotDesktopLaunchBuilder { @@ -11,7 +12,7 @@ object ProotDesktopLaunchBuilder { context: Context, runtime: ProotRuntime, rootfs: File, - x11SocketDirectory: File, + x11Endpoint: X11DisplayEndpoint, environment: DesktopEnvironment, configuration: DesktopConfiguration, audioEndpoint: AudioEndpoint? = null, @@ -19,13 +20,14 @@ object ProotDesktopLaunchBuilder { require(File(rootfs, RootfsInstallationPipeline.READY_MARKER).isFile) { "The selected Linux image is not ready" } - require(x11SocketDirectory.isDirectory) { "The X11 socket directory is unavailable" } + require(x11Endpoint.socketDirectory.isDirectory) { "The X11 socket directory is unavailable" } val guestHome = if (File(rootfs, "root").isDirectory) "/root" else "/" val arguments = buildArguments( prootPath = runtime.executable.absolutePath, rootfsPath = ProotPathContract.rootfsPath(context, rootfs), - x11SocketDirectory = x11SocketDirectory.absolutePath, + x11SocketDirectory = x11Endpoint.socketDirectory.absolutePath, + bindX11Socket = x11Endpoint.requiresGuestBind, guestHome = guestHome, environment = environment, configuration = configuration, @@ -64,6 +66,7 @@ object ProotDesktopLaunchBuilder { prootPath: String, rootfsPath: String, x11SocketDirectory: String, + bindX11Socket: Boolean = true, guestHome: String, environment: DesktopEnvironment, configuration: DesktopConfiguration, @@ -77,8 +80,10 @@ object ProotDesktopLaunchBuilder { add("--root-id") add("--rootfs=$rootfsPath") addAndroidProotBindMounts() - add("-b") - add("$x11SocketDirectory:/tmp/.X11-unix") + if (bindX11Socket) { + add("-b") + add("$x11SocketDirectory:/tmp/.X11-unix") + } if (audioAuthDirectory != null) { add("-b") add("$audioAuthDirectory:${AudioEndpoint.GUEST_AUTH_DIRECTORY}") diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt index d03bb93..95d2903 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt @@ -4,6 +4,7 @@ import android.content.Context import org.randomcoder.udroid.audio.AudioEndpoint import org.randomcoder.udroid.install.ProotRuntime import org.randomcoder.udroid.install.RootfsInstallationPipeline +import org.randomcoder.udroid.x11.X11DisplayEndpoint import java.io.File import java.nio.file.Files @@ -27,7 +28,7 @@ object ProotTerminalLaunchBuilder { context: Context, runtime: ProotRuntime, rootfs: File = InstalledRootfsResolver.resolve(context), - x11SocketDirectory: File? = null, + x11Endpoint: X11DisplayEndpoint? = null, audioEndpoint: AudioEndpoint? = null, ): ProotTerminalLaunch { require(File(rootfs, RootfsInstallationPipeline.READY_MARKER).isFile) { @@ -57,7 +58,8 @@ object ProotTerminalLaunchBuilder { rootfsPath = ProotPathContract.rootfsPath(context, rootfs), guestHome = guestHome, guestShell = guestShell, - x11SocketDirectory = x11SocketDirectory?.absolutePath, + x11SocketDirectory = x11Endpoint?.socketDirectory?.absolutePath, + bindX11Socket = x11Endpoint?.requiresGuestBind == true, audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, ) val environment = @@ -83,6 +85,7 @@ object ProotTerminalLaunchBuilder { guestHome: String, guestShell: String, x11SocketDirectory: String? = null, + bindX11Socket: Boolean = x11SocketDirectory != null, audioAuthDirectory: String? = null, ): Array = buildList { @@ -94,7 +97,7 @@ object ProotTerminalLaunchBuilder { add("--root-id") add("--rootfs=$rootfsPath") addAndroidProotBindMounts() - if (x11SocketDirectory != null) { + if (x11SocketDirectory != null && bindX11Socket) { add("-b") add("$x11SocketDirectory:/tmp/.X11-unix") } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index c531ba1..2add234 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -442,8 +442,8 @@ class RuntimeSupervisorService : Service() { return } - x11Controller.whenReady { socketDirectory -> - if (socketDirectory == null) { + x11Controller.whenReady { x11Endpoint -> + if (x11Endpoint == null) { callback(Result.failure(IllegalStateException("Embedded X11 failed to start"))) return@whenReady } @@ -456,7 +456,7 @@ class RuntimeSupervisorService : Service() { context = this, runtime = ProotRuntimeInstaller.install(this), rootfs = rootfs, - x11SocketDirectory = socketDirectory, + x11Endpoint = x11Endpoint, application = application, audioEndpoint = audioController.endpoint(), ) @@ -577,9 +577,9 @@ class RuntimeSupervisorService : Service() { "touch_scale" to request.configuration.touchScaleEnabled, ), ) - x11Controller.whenReady { socketDirectory -> + x11Controller.whenReady { x11Endpoint -> if (desktopLaunchToken.get() != launchToken) return@whenReady - if (socketDirectory == null) { + if (x11Endpoint == null) { if (desktopLaunchToken.compareAndSet(launchToken, null)) { publishDesktopFailure(request, "Embedded X11 display :0 is unavailable") } @@ -595,7 +595,7 @@ class RuntimeSupervisorService : Service() { guestX11TransportProbe.query( runtime = prootRuntime, rootfs = rootfs, - socketDirectory = socketDirectory, + endpoint = x11Endpoint, ) ) { is GuestX11TransportResult.Ready -> @@ -611,7 +611,7 @@ class RuntimeSupervisorService : Service() { mapOf( "rootfs" to request.rootfsName, "display" to DISPLAY_NUMBER, - "transport" to "private_bind", + "transport" to x11Endpoint.transport.journalValue, ), ) @@ -626,7 +626,7 @@ class RuntimeSupervisorService : Service() { mapOf( "rootfs" to request.rootfsName, "display" to DISPLAY_NUMBER, - "transport" to "private_bind", + "transport" to x11Endpoint.transport.journalValue, "stage" to probe.stage, "errno" to probe.errno, ), @@ -640,7 +640,7 @@ class RuntimeSupervisorService : Service() { context = this, runtime = prootRuntime, rootfs = rootfs, - x11SocketDirectory = socketDirectory, + x11Endpoint = x11Endpoint, environment = request.environment, configuration = request.configuration, audioEndpoint = audioController.endpoint(), @@ -1022,12 +1022,12 @@ class RuntimeSupervisorService : Service() { fields = mapOf("exception" to error.javaClass.name), ) } - val x11SocketDirectory = x11Controller.start(rootfs, bootId) + val x11Endpoint = x11Controller.start(rootfs, bootId) ProotTerminalLaunchBuilder.create( context = this, runtime = runtime, rootfs = rootfs, - x11SocketDirectory = x11SocketDirectory, + x11Endpoint = x11Endpoint, audioEndpoint = audioController.endpoint(), ) }.mapCatching { launch -> diff --git a/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt b/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt index d15cd09..988d68d 100644 --- a/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt +++ b/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt @@ -42,14 +42,15 @@ internal class GuestX11TransportProbe(private val context: Context) { fun query( runtime: ProotRuntime, rootfs: File, - socketDirectory: File, + endpoint: X11DisplayEndpoint, ): GuestX11TransportResult { val nativeProbe = NativeProbeInstaller.install(context) val arguments = GuestX11ProbeCommand.buildArguments( prootPath = runtime.executable.absolutePath, rootfsPath = ProotPathContract.rootfsPath(context, rootfs), - socketDirectory = socketDirectory.absolutePath, + socketDirectory = endpoint.socketDirectory.absolutePath, + bindSocket = endpoint.requiresGuestBind, nativeProbe = nativeProbe.absolutePath, forceDenied = BuildConfig.X11_GUEST_PROBE_FAULT == "deny", ) @@ -105,6 +106,7 @@ internal object GuestX11ProbeCommand { prootPath: String, rootfsPath: String, socketDirectory: String, + bindSocket: Boolean, nativeProbe: String, forceDenied: Boolean, androidBindMounts: List = ANDROID_PROOT_BIND_MOUNTS, @@ -120,8 +122,10 @@ internal object GuestX11ProbeCommand { add("-b") add(path) } - add("-b") - add("$socketDirectory:/tmp/.X11-unix") + if (bindSocket) { + add("-b") + add("$socketDirectory:/tmp/.X11-unix") + } add("-b") add("$nativeProbe:$GUEST_PROBE") add("--cwd=/") diff --git a/app/src/main/java/org/randomcoder/udroid/x11/X11DisplayEndpoint.kt b/app/src/main/java/org/randomcoder/udroid/x11/X11DisplayEndpoint.kt new file mode 100644 index 0000000..7d322f9 --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/x11/X11DisplayEndpoint.kt @@ -0,0 +1,17 @@ +package org.randomcoder.udroid.x11 + +import java.io.File + +enum class X11GuestTransport(val journalValue: String) { + DIRECT_ROOTFS("direct_rootfs"), + PRIVATE_BIND("private_bind"), +} + +data class X11DisplayEndpoint( + val runtimeDirectory: File, + val socketDirectory: File, + val transport: X11GuestTransport, +) { + val requiresGuestBind: Boolean + get() = transport == X11GuestTransport.PRIVATE_BIND +} diff --git a/app/src/main/java/org/randomcoder/udroid/x11/X11ServerController.kt b/app/src/main/java/org/randomcoder/udroid/x11/X11ServerController.kt index 4fbb394..689728d 100644 --- a/app/src/main/java/org/randomcoder/udroid/x11/X11ServerController.kt +++ b/app/src/main/java/org/randomcoder/udroid/x11/X11ServerController.kt @@ -11,6 +11,7 @@ import android.os.Looper import android.os.Message import android.os.Messenger import android.os.ParcelFileDescriptor +import android.system.Os import org.randomcoder.udroid.runtime.EventJournal import java.io.File @@ -33,7 +34,7 @@ class X11ServerController( private var bound = false private var pendingStart: StartRequest? = null private var rendererCallback: ((ParcelFileDescriptor?) -> Unit)? = null - private val readyCallbacks = mutableListOf<(File?) -> Unit>() + private val readyCallbacks = mutableListOf<(X11DisplayEndpoint?) -> Unit>() private var rendererRequestInFlight = false private var ready = false @@ -71,14 +72,10 @@ class X11ServerController( fun start( rootfs: File, bootId: String, - ): File { - val runtimeDirectory = X11RuntimePaths.runtimeDirectory(context) - val socketDirectory = X11RuntimePaths.socketDirectory(context) - check(socketDirectory.mkdirs() || socketDirectory.isDirectory) { - "Could not prepare the private X11 socket directory" - } + ): X11DisplayEndpoint { + val endpoint = selectEndpoint(rootfs, bootId) val xkbRoot = File(rootfs, "usr/share/X11/xkb") - val request = StartRequest(runtimeDirectory, xkbRoot, bootId) + val request = StartRequest(endpoint, xkbRoot, bootId) pendingStart = request journal.append( @@ -90,7 +87,8 @@ class X11ServerController( fields = mapOf( "protocol_version" to X11ServerProtocol.VERSION, - "socket" to X11RuntimePaths.displaySocket(context).absolutePath, + "socket" to File(endpoint.socketDirectory, "X0").absolutePath, + "transport" to endpoint.transport.journalValue, ), ) @@ -105,7 +103,7 @@ class X11ServerController( ) check(bound) { "Android rejected the embedded X11 service binding" } } - return socketDirectory + return endpoint } fun stop(bootId: String?) { @@ -141,16 +139,16 @@ class X11ServerController( if (ready && !rendererRequestInFlight) sendRendererRequest() } - fun activeSocketDirectory(): File? = + fun activeEndpoint(): X11DisplayEndpoint? = pendingStart ?.takeIf { ready } - ?.let { X11RuntimePaths.socketDirectory(context) } - ?.takeIf(File::isDirectory) + ?.endpoint + ?.takeIf { it.socketDirectory.isDirectory } - fun whenReady(callback: (File?) -> Unit) { - val socketDirectory = activeSocketDirectory() - if (socketDirectory != null) { - callback(socketDirectory) + fun whenReady(callback: (X11DisplayEndpoint?) -> Unit) { + val endpoint = activeEndpoint() + if (endpoint != null) { + callback(endpoint) } else { readyCallbacks += callback } @@ -167,7 +165,7 @@ class X11ServerController( Bundle().apply { putString( X11ServerProtocol.KEY_RUNTIME_DIRECTORY, - request.runtimeDirectory.absolutePath, + request.endpoint.runtimeDirectory.absolutePath, ) putString( X11ServerProtocol.KEY_XKB_ROOT, @@ -197,6 +195,9 @@ class X11ServerController( buildMap { put("protocol_version", X11ServerProtocol.VERSION) put("server_pid", data.getInt(X11ServerProtocol.KEY_PID)) + pendingStart?.endpoint?.transport?.journalValue?.let { + put("transport", it) + } data.getString(X11ServerProtocol.KEY_SOCKET_PATH)?.let { put("socket", it) } @@ -217,9 +218,9 @@ class X11ServerController( ) if (state == X11ServerProtocol.STATE_READY) { ready = true - val socketDirectory = activeSocketDirectory() + val endpoint = activeEndpoint() readyCallbacks.toList().also(readyCallbacks::removeAll).forEach { - it(socketDirectory) + it(endpoint) } if (rendererCallback != null && !rendererRequestInFlight) { sendRendererRequest() @@ -284,9 +285,71 @@ class X11ServerController( ) } + private fun selectEndpoint( + rootfs: File, + bootId: String, + ): X11DisplayEndpoint = + runCatching { directRootfsEndpoint(rootfs) } + .getOrElse { error -> + journal.append( + component = "x11", + severity = "warning", + event = "direct_transport_unavailable", + message = error.message ?: "Could not prepare direct rootfs X11 transport", + bootId = bootId, + fields = mapOf("fallback" to X11GuestTransport.PRIVATE_BIND.journalValue), + ) + privateBindEndpoint() + } + + private fun directRootfsEndpoint(rootfs: File): X11DisplayEndpoint { + val root = rootfs.canonicalFile + val runtime = File(root, "tmp") + check(runtime.mkdirs() || runtime.isDirectory) { + "Could not prepare the rootfs X11 runtime directory" + } + val canonicalRuntime = runtime.canonicalFile + check(canonicalRuntime.toPath().startsWith(root.toPath())) { + "The rootfs X11 runtime directory escaped the installed image" + } + Os.chmod(canonicalRuntime.absolutePath, ROOTFS_TMP_MODE) + val socketDirectory = File(canonicalRuntime, ".X11-unix") + check(socketDirectory.mkdirs() || socketDirectory.isDirectory) { + "Could not prepare the rootfs X11 socket directory" + } + val canonicalSocketDirectory = socketDirectory.canonicalFile + check(canonicalSocketDirectory.toPath().startsWith(root.toPath())) { + "The rootfs X11 socket directory escaped the installed image" + } + // PRoot can leave a mode-000 bind target behind after unmounting it. + Os.chmod(canonicalSocketDirectory.absolutePath, ROOTFS_TMP_MODE) + return X11DisplayEndpoint( + runtimeDirectory = canonicalRuntime, + socketDirectory = canonicalSocketDirectory, + transport = X11GuestTransport.DIRECT_ROOTFS, + ) + } + + private fun privateBindEndpoint(): X11DisplayEndpoint { + val runtime = X11RuntimePaths.runtimeDirectory(context) + val socketDirectory = X11RuntimePaths.socketDirectory(context) + check(socketDirectory.mkdirs() || socketDirectory.isDirectory) { + "Could not prepare the private X11 socket directory" + } + return X11DisplayEndpoint( + runtimeDirectory = runtime, + socketDirectory = socketDirectory, + transport = X11GuestTransport.PRIVATE_BIND, + ) + } + private data class StartRequest( - val runtimeDirectory: File, + val endpoint: X11DisplayEndpoint, val xkbRoot: File, val bootId: String, ) + + private companion object { + const val ROOTFS_TMP_MODE = 0x3ff + } } diff --git a/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt b/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt index a692938..36f184e 100644 --- a/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt +++ b/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt @@ -22,6 +22,8 @@ class X11ServerService : Service() { } private val startAccepted = AtomicBoolean(false) private val serverReady = AtomicBoolean(false) + @Volatile + private var activeSocket: File? = null private val handler by lazy { Handler(mainLooper) { message -> when (message.what) { @@ -61,7 +63,7 @@ class X11ServerService : Service() { return } if (!startAccepted.compareAndSet(false, true)) { - val socket = X11RuntimePaths.displaySocket(this) + val socket = activeSocket ?: X11RuntimePaths.displaySocket(this) val probe = X11ProtocolProbe.query(socket) reply( client, @@ -88,6 +90,7 @@ class X11ServerService : Service() { }.let(::File) prepareRuntime(runtimeDirectory, xkbRoot) val socket = File(runtimeDirectory, ".X11-unix/X0") + activeSocket = socket reply( client, X11ServerProtocol.STATE_STARTING, @@ -132,7 +135,7 @@ class X11ServerService : Service() { } val socketDirectory = File(runtime, ".X11-unix") check(socketDirectory.mkdirs() || socketDirectory.isDirectory) { - "Could not create the private X11 socket directory" + "Could not create the X11 socket directory" } val staleSocket = File(socketDirectory, "X0") check(!staleSocket.exists() || staleSocket.delete()) { diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt index 6e25bb4..445f20f 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt @@ -70,6 +70,7 @@ class DesktopEnvironmentScannerTest { prootPath = "/data/proot", rootfsPath = "/data/rootfs", x11SocketDirectory = "/data/x11", + bindX11Socket = false, guestHome = "/root", environment = desktop, configuration = @@ -87,6 +88,7 @@ class DesktopEnvironmentScannerTest { assertTrue(arguments.contains("PULSE_SERVER=tcp:127.0.0.1:4713")) assertTrue(arguments.contains("PULSE_COOKIE=/tmp/.udroid-pulse/cookie")) assertTrue(arguments.contains("GDK_SCALE=2")) + assertFalse(arguments.any { "/data/x11" in it }) val script = arguments[arguments.indexOf("-lc") + 1] assertTrue(script.contains("-s false")) assertTrue(script.contains("sleep 1")) diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/ProotApplicationLaunchTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/ProotApplicationLaunchTest.kt index c2a53a8..797db3c 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/ProotApplicationLaunchTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/ProotApplicationLaunchTest.kt @@ -38,4 +38,22 @@ class ProotApplicationLaunchTest { assertFalse(arguments.contains("sh")) assertFalse(arguments.contains("-c")) } + + @Test + fun `direct rootfs display does not add an X11 bind alias`() { + val socketDirectory = "/data/rootfs/tmp/.X11-unix" + val arguments = + ProotApplicationLaunchBuilder.buildArguments( + prootPath = "/data/proot", + rootfsPath = "/data/rootfs", + x11SocketDirectory = socketDirectory, + bindX11Socket = false, + guestHome = "/root", + guestWorkingDirectory = "/root", + applicationArguments = listOf("/usr/bin/demo"), + ) + + assertTrue("DISPLAY=:0" in arguments) + assertTrue(arguments.none { socketDirectory in it }) + } } diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt index a291d2c..e6b40a2 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt @@ -101,6 +101,24 @@ class ProotTerminalLaunchTest { assertTrue("DISPLAY=:0" in arguments) } + @Test + fun `direct rootfs X11 exports display without a bind alias`() { + val socketDirectory = "/data/rootfs/tmp/.X11-unix" + val arguments = + ProotTerminalLaunchBuilder.buildArguments( + linker = "linker64", + prootPath = "proot", + rootfsPath = "/data/rootfs", + guestHome = "/root", + guestShell = "/bin/sh", + x11SocketDirectory = socketDirectory, + bindX11Socket = false, + ) + + assertTrue("DISPLAY=:0" in arguments) + assertTrue(arguments.none { socketDirectory in it }) + } + @Test fun `authenticated loopback audio is exported to the guest`() { val arguments = diff --git a/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt b/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt index 6a0fdb5..5d65300 100644 --- a/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt @@ -34,6 +34,7 @@ class GuestX11TransportProbeTest { prootPath = "/data/proot", rootfsPath = "/data/rootfs", socketDirectory = "/data/x11/.X11-unix", + bindSocket = true, nativeProbe = "/data/runtime_probe", forceDenied = false, androidBindMounts = listOf("/system", "/proc"), @@ -60,6 +61,7 @@ class GuestX11TransportProbeTest { prootPath = "/data/proot", rootfsPath = "/data/rootfs", socketDirectory = "/data/x11/.X11-unix", + bindSocket = false, nativeProbe = "/data/runtime_probe", forceDenied = true, androidBindMounts = emptyList(), @@ -67,5 +69,6 @@ class GuestX11TransportProbeTest { ) assertEquals("--x11-deny", arguments[arguments.lastIndex - 1]) + assertTrue("/data/x11/.X11-unix:/tmp/.X11-unix" !in arguments) } } diff --git a/docs/LINUX_APPLICATION_LAUNCHER.md b/docs/LINUX_APPLICATION_LAUNCHER.md index 7d3fb65..755a2c1 100644 --- a/docs/LINUX_APPLICATION_LAUNCHER.md +++ b/docs/LINUX_APPLICATION_LAUNCHER.md @@ -63,7 +63,8 @@ Graphical applications receive an isolated environment containing: - `XDG_CURRENT_DESKTOP=UDROID` - `GDK_BACKEND=x11` - `QT_QPA_PLATFORM=xcb` -- the same private X11 socket bind used by the interactive distro +- the active X11 endpoint selected for the distro: normally the socket living + directly at `/tmp/.X11-unix/X0`, or the validated private-bind fallback Each application is a separate PRoot process owned by `RuntimeSupervisorService`. Output is drained off the process pipe into the diff --git a/docs/X11_RUNTIME_ARCHITECTURE.md b/docs/X11_RUNTIME_ARCHITECTURE.md index 609676a..bb51f0b 100644 --- a/docs/X11_RUNTIME_ARCHITECTURE.md +++ b/docs/X11_RUNTIME_ARCHITECTURE.md @@ -1,6 +1,6 @@ # Embedded X11 runtime architecture -Status: accepted integration boundary, 2026-07-24 +Status: accepted integration boundary, updated 2026-08-13 ## Decision @@ -47,9 +47,9 @@ supervised X server and PRoot guest continue running. flowchart LR UI["uDroid desktop Activity"] -->|"attach Surface and IME"| View["Lorie display view"] Supervisor["Runtime supervisor"] -->|"versioned Binder contract"| Server["X11 server process"] - Server --> Socket["Private X socket"] + Server --> Socket["X0 inside active rootfs /tmp"] Supervisor --> PRoot["PRoot distro"] - Socket -->|"bind-mounted as /tmp/.X11-unix/X0"| PRoot + Socket -->|"visible directly as /tmp/.X11-unix/X0"| PRoot PRoot -->|"DISPLAY=:0"| Apps["Linux X11 apps"] Server -->|"buffer and event FD transport"| View View -->|"EGL/GLES present"| Surface["Android Surface"] @@ -62,8 +62,13 @@ flowchart LR desktop Activity and its `Surface`. - The desktop Activity owns only the visible Android surface and input/IME adapters. Closing or rotating it must not terminate Linux or the X server. -- The X socket and control socket live in a per-generation private runtime - directory. That directory is bind-mounted into PRoot as `/tmp`. +- The X socket lives directly in the active rootfs at + `/tmp/.X11-unix/X0`. This avoids relying on PRoot to translate an + app-private Unix-socket directory on OEM Android builds. The old private + directory transport remains a preparation-time fallback and must pass the + same guest handshake before a desktop is launched. +- The Binder control and renderer channels remain app-private and independent + from the guest filesystem. - The control protocol uses Binder and file descriptors. It is never exposed on TCP and never accepts an unauthenticated broadcast. @@ -136,18 +141,20 @@ Desktop environments are not the first health test. Each gate records latency, RSS/PSS, CPU time, frame/present counts, and failure reason. 1. `server-start`: the native process reaches the Xorg ready state. -2. `socket-ready`: the private `X0` socket exists and accepts a connection. -3. `x-query`: a tiny client reads the root window geometry and required +2. `socket-ready`: the host-side `X0` path completes an X11 setup handshake. +3. `guest-socket-ready`: the same packaged native probe runs through the exact + PRoot namespace and completes setup against `/tmp/.X11-unix/X0`. +4. `x-query`: a tiny client reads the root window geometry and required extensions. -4. `test-pattern`: the server draws a deterministic pattern with a known +5. `test-pattern`: the server draws a deterministic pattern with a known checksum and the Android surface receives frames. -5. `surface-cycle`: attach, resize, detach, and reattach without restarting +6. `surface-cycle`: attach, resize, detach, and reattach without restarting the server. -6. `input-loop`: injected pointer and key events are observed by a tiny X +7. `input-loop`: injected pointer and key events are observed by a tiny X client. -7. `present-soak`: bounded frame pacing and buffer lifetime test. -8. `xterm`: first real guest application. -9. `lightweight-session`: first desktop, initially without composition. +8. `present-soak`: bounded frame pacing and buffer lifetime test. +9. `xterm`: first real guest application. +10. `lightweight-session`: first desktop, initially without composition. GNOME, KDE, browsers, and games remain later macro probes. @@ -159,12 +166,12 @@ visible application presentation, motion, and surface-cycle checks: | Probe | Result | | --- | --- | | Process isolation | `org.randomcoder.udroid:x11` owned the X server | -| Server socket | app-private `.X11-unix/X0`, mode `srwxrwxrwx` | +| Server socket (historical run) | app-private `.X11-unix/X0`, mode `srwxrwxrwx` | | Protocol setup | X11 `11.0` connection setup completed | | Cold Binder/process setup | 708 ms | | Native Xorg to protocol-ready | 117 ms | | Total request to protocol-ready | 825 ms | -| Guest bridge | same socket bind-mounted at `/tmp/.X11-unix`, `DISPLAY=:0` | +| Guest bridge | historical private socket bind at `/tmp/.X11-unix`, `DISPLAY=:0` | | Renderer transport | one Binder-delivered renderer FD per Desktop attach | | Android display target | 1080×2142 below expanded controls; near-full height with the 32 dp strip | | Live X11 geometry | raw probe observed 1280×720 in Fixed mode and 1080×2142 after returning to Native | @@ -193,6 +200,30 @@ parses the eight-byte setup response. Merely connecting to the Unix socket is not considered ready; that weaker test initially hid a probe-side `LocalSocket` ordering bug. +## Guest transport compatibility checkpoint + +Issue 23 showed that host readiness is insufficient: the Android process can +connect to its socket while the PRoot-visible bind alias fails with `EACCES`. +uDroid now runs the packaged protocol probe inside the exact PRoot namespace +before launching a desktop. A failure records its stage and errno, leaves the +display unclaimed, and never emits `desktop_started`. + +The primary route creates `X0` directly under the active rootfs. Startup also +repairs `/tmp` and `/tmp/.X11-unix` to mode `01777`, because an earlier PRoot +bind may leave a mode-000 mount-point placeholder behind. Terminal, desktop, +and graphical-application launchers omit the X11 bind for this route. + +The API 34 AVD regression test proved both directions: + +- injected guest `EACCES`: `guest_transport_failed`, errno 13, no desktop + process; +- normal direct-rootfs route: X11 11.0 guest handshake followed by + `desktop_started`, with no X11 directory bind in either PRoot command. + +The exact Huawei policy or PRoot behavior still requires confirmation on the +reporter's Android 12 device. The diagnostic contract no longer confuses that +transport failure with XFCE, D-Bus, compositing, or GPU startup. + The presentation investigation used two independent observations. An XWD dump of the root window contained the expected gears while Android was still black, which isolated the failure to the Android surface rather than GLX or Xorg. The From b030b27653d67e375e5632a4dd850bbfa44c44f0 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Fri, 14 Aug 2026 09:49:56 +0530 Subject: [PATCH 3/3] test(x11): expand guest transport diagnostics --- .../assets/runtime/arm64-v8a/runtime_probe | Bin 7648 -> 11624 bytes .../assets/runtime/armeabi-v7a/runtime_probe | Bin 5788 -> 10096 bytes .../main/assets/runtime/x86_64/runtime_probe | Bin 7576 -> 11064 bytes app/src/main/cpp/runtime_probe.c | 70 +++-- .../udroid/runtime/NativeProbeInstaller.kt | 2 +- .../runtime/RuntimeSupervisorService.kt | 90 +++++- .../udroid/x11/GuestX11TransportProbe.kt | 289 ++++++++++++++++-- .../udroid/x11/GuestX11TransportProbeTest.kt | 68 ++++- 8 files changed, 466 insertions(+), 53 deletions(-) diff --git a/app/src/main/assets/runtime/arm64-v8a/runtime_probe b/app/src/main/assets/runtime/arm64-v8a/runtime_probe index 49d377dc407b824c26df60715ff6b726b8a0188a..9c3b3b1837cd26feec5ed15b4cfe9f965d27b112 100755 GIT binary patch literal 11624 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 literal 7648 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 Tp1$uQuFBn|;kQgmf+zQ1F@w}Z literal 5788 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 M4ZC0a0Nl6xH+z%rGXMYp diff --git a/app/src/main/assets/runtime/x86_64/runtime_probe b/app/src/main/assets/runtime/x86_64/runtime_probe index ef445d77a0533443e276dfad97a9938630a8cd8b..1c511d5a891f90af3f6fa3a7e9b43e64897c0d0a 100755 GIT binary patch literal 11064 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 z`` #include +#include #include #include #include @@ -41,32 +42,43 @@ static int read_exactly(int fd, uint8_t *buffer, size_t count) { return 0; } -static int probe_x11_socket(const char *path, int force_denied) { +static int probe_x11_socket(const char *path, int force_denied, + int abstract_socket, const char *socket_namespace) { static const uint8_t setup_request[] = { 0x6c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; uint8_t setup_header[8]; struct sockaddr_un address; + socklen_t address_size; struct timeval timeout = {.tv_sec = 1, .tv_usec = 0}; + const int64_t started_ms = monotonic_ms(); if (force_denied) { printf("{\"event\":\"x11_guest_probe\",\"status\":\"connect_failed\"," - "\"errno\":%d,\"detail\":\"Permission denied (injected)\"}\n", - EACCES); + "\"socket_namespace\":\"%s\",\"errno\":%d," + "\"address_bytes\":0,\"elapsed_ms\":0," + "\"detail\":\"Permission denied (injected)\"}\n", + socket_namespace, EACCES); return 20; } if (strlen(path) >= sizeof(address.sun_path)) { printf("{\"event\":\"x11_guest_probe\",\"status\":\"invalid_path\"," - "\"errno\":%d,\"detail\":\"Socket path is too long\"}\n", - ENAMETOOLONG); + "\"socket_namespace\":\"%s\",\"errno\":%d," + "\"address_bytes\":0,\"elapsed_ms\":%lld," + "\"detail\":\"Socket path is too long\"}\n", + socket_namespace, ENAMETOOLONG, + (long long)(monotonic_ms() - started_ms)); return 21; } int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { printf("{\"event\":\"x11_guest_probe\",\"status\":\"socket_failed\"," - "\"errno\":%d,\"detail\":\"%s\"}\n", errno, strerror(errno)); + "\"socket_namespace\":\"%s\",\"errno\":%d," + "\"address_bytes\":0,\"elapsed_ms\":%lld,\"detail\":\"%s\"}\n", + socket_namespace, errno, + (long long)(monotonic_ms() - started_ms), strerror(errno)); return 22; } (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); @@ -74,12 +86,23 @@ static int probe_x11_socket(const char *path, int force_denied) { memset(&address, 0, sizeof(address)); address.sun_family = AF_UNIX; - memcpy(address.sun_path, path, strlen(path) + 1); - if (connect(fd, (struct sockaddr *)&address, sizeof(address)) != 0) { + if (abstract_socket) { + size_t path_length = strlen(path); + address.sun_path[0] = '\0'; + memcpy(address.sun_path + 1, path, path_length); + address_size = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + path_length); + } else { + memcpy(address.sun_path, path, strlen(path) + 1); + /* libxcb 1.14 passes the complete structure for filesystem sockets. */ + address_size = sizeof(address); + } + if (connect(fd, (struct sockaddr *)&address, address_size) != 0) { int saved_errno = errno; printf("{\"event\":\"x11_guest_probe\",\"status\":\"connect_failed\"," - "\"errno\":%d,\"detail\":\"%s\"}\n", - saved_errno, strerror(saved_errno)); + "\"socket_namespace\":\"%s\",\"errno\":%d," + "\"address_bytes\":%u,\"elapsed_ms\":%lld,\"detail\":\"%s\"}\n", + socket_namespace, saved_errno, (unsigned int)address_size, + (long long)(monotonic_ms() - started_ms), strerror(saved_errno)); close(fd); return 23; } @@ -87,8 +110,11 @@ static int probe_x11_socket(const char *path, int force_denied) { read_exactly(fd, setup_header, sizeof(setup_header)) != 0) { int saved_errno = errno; printf("{\"event\":\"x11_guest_probe\",\"status\":\"handshake_failed\"," - "\"errno\":%d,\"detail\":\"%s\"}\n", - saved_errno, saved_errno == 0 ? "Connection closed" : strerror(saved_errno)); + "\"socket_namespace\":\"%s\",\"errno\":%d," + "\"address_bytes\":%u,\"elapsed_ms\":%lld,\"detail\":\"%s\"}\n", + socket_namespace, saved_errno, (unsigned int)address_size, + (long long)(monotonic_ms() - started_ms), + saved_errno == 0 ? "Connection closed" : strerror(saved_errno)); close(fd); return 24; } @@ -100,21 +126,31 @@ static int probe_x11_socket(const char *path, int force_denied) { (unsigned int)setup_header[4] | ((unsigned int)setup_header[5] << 8U); if (setup_header[0] != 1) { printf("{\"event\":\"x11_guest_probe\",\"status\":\"rejected\"," - "\"setup_status\":%u,\"protocol_major\":%u,\"protocol_minor\":%u}\n", - (unsigned int)setup_header[0], protocol_major, protocol_minor); + "\"socket_namespace\":\"%s\",\"setup_status\":%u," + "\"protocol_major\":%u,\"protocol_minor\":%u," + "\"address_bytes\":%u,\"elapsed_ms\":%lld}\n", + socket_namespace, (unsigned int)setup_header[0], protocol_major, protocol_minor, + (unsigned int)address_size, (long long)(monotonic_ms() - started_ms)); return 25; } printf("{\"event\":\"x11_guest_probe\",\"status\":\"ready\"," - "\"protocol_major\":%u,\"protocol_minor\":%u}\n", - protocol_major, protocol_minor); + "\"socket_namespace\":\"%s\",\"protocol_major\":%u," + "\"protocol_minor\":%u,\"address_bytes\":%u,\"elapsed_ms\":%lld}\n", + socket_namespace, protocol_major, protocol_minor, (unsigned int)address_size, + (long long)(monotonic_ms() - started_ms)); return 0; } int main(int argc, char **argv) { + if (argc >= 3 && strcmp(argv[1], "--x11-abstract") == 0) { + setvbuf(stdout, NULL, _IOLBF, 0); + return probe_x11_socket(argv[2], 0, 1, "abstract"); + } if (argc >= 3 && (strcmp(argv[1], "--x11") == 0 || strcmp(argv[1], "--x11-deny") == 0)) { setvbuf(stdout, NULL, _IOLBF, 0); - return probe_x11_socket(argv[2], strcmp(argv[1], "--x11-deny") == 0); + return probe_x11_socket(argv[2], strcmp(argv[1], "--x11-deny") == 0, + 0, "filesystem"); } const char *boot_id = argc > 1 ? argv[1] : "unknown"; diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt b/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt index a759b31..4f2b1d4 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/NativeProbeInstaller.kt @@ -6,7 +6,7 @@ import java.io.File import java.io.FileOutputStream object NativeProbeInstaller { - private const val PROBE_VERSION = "2" + private const val PROBE_VERSION = "4" private val supportedAbis = setOf("arm64-v8a", "armeabi-v7a", "x86_64") fun install(context: Context): File { diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index 2add234..ee4a834 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -18,6 +18,7 @@ import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.ParcelFileDescriptor +import android.os.SystemClock import android.system.ErrnoException import android.system.Os import android.system.OsConstants @@ -577,6 +578,7 @@ class RuntimeSupervisorService : Service() { "touch_scale" to request.configuration.touchScaleEnabled, ), ) + val desktopRequestedAtMs = SystemClock.elapsedRealtime() x11Controller.whenReady { x11Endpoint -> if (desktopLaunchToken.get() != launchToken) return@whenReady if (x11Endpoint == null) { @@ -590,14 +592,87 @@ class RuntimeSupervisorService : Service() { if (desktopLaunchToken.get() != launchToken) return@runCatching null val rootfs = InstalledRootfsResolver.resolve(this, request.rootfsName) val prootRuntime = ProotRuntimeInstaller.install(this) - when ( - val probe = - guestX11TransportProbe.query( - runtime = prootRuntime, - rootfs = rootfs, - endpoint = x11Endpoint, + val probeReport = + guestX11TransportProbe.query( + runtime = prootRuntime, + rootfs = rootfs, + endpoint = x11Endpoint, + ) + listOf( + "abstract" to probeReport.abstractSocket, + "filesystem" to probeReport.filesystemSocket, + ).forEach { (socketNamespace, result) -> + val resultFields = + mutableMapOf( + "rootfs" to request.rootfsName, + "display" to DISPLAY_NUMBER, + "endpoint_transport" to x11Endpoint.transport.journalValue, + "socket_namespace" to socketNamespace, + "guest_socket" to "/tmp/.X11-unix/X0", ) - ) { + val message = + when (result) { + is GuestX11TransportResult.Ready -> { + resultFields["status"] = "ready" + resultFields["protocol_major"] = result.protocolMajor + resultFields["protocol_minor"] = result.protocolMinor + resultFields["address_bytes"] = result.addressBytes + resultFields["elapsed_ms"] = result.elapsedMs + "Guest $socketNamespace X11 probe completed protocol " + + "${result.protocolMajor}.${result.protocolMinor} setup" + } + + is GuestX11TransportResult.Failed -> { + resultFields["status"] = result.stage + resultFields["errno"] = result.errno + resultFields["detail"] = result.detail + resultFields["address_bytes"] = result.addressBytes + resultFields["elapsed_ms"] = result.elapsedMs + "Guest $socketNamespace X11 probe ${result.stage}: ${result.detail}" + } + } + app.journal.append( + component = "x11", + severity = + if ( + socketNamespace == "filesystem" && + result is GuestX11TransportResult.Failed + ) { + "error" + } else { + "info" + }, + event = "guest_transport_probe", + message = message, + bootId = runtime.bootId, + fields = resultFields, + ) + } + val clientProbe = probeReport.client + app.journal.append( + component = "x11", + severity = if (clientProbe.status == "ready") "info" else "warning", + event = "guest_xrdb_probe", + message = + when (clientProbe.status) { + "ready" -> "Guest xrdb connected through the distro X11 libraries" + "unavailable" -> "Guest xrdb is unavailable" + "timeout" -> "Guest xrdb connection timed out" + else -> "Guest xrdb connection failed: ${clientProbe.output.ifBlank { clientProbe.status }}" + }, + bootId = runtime.bootId, + fields = + buildMap { + put("rootfs", request.rootfsName) + put("display", DISPLAY_NUMBER) + put("endpoint_transport", x11Endpoint.transport.journalValue) + put("status", clientProbe.status) + put("probe_process_exit", clientProbe.exitCode) + put("elapsed_ms", clientProbe.elapsedMs) + clientProbe.fields.forEach { (key, value) -> put(key, value) } + }, + ) + when (val probe = probeReport.filesystemSocket) { is GuestX11TransportResult.Ready -> app.journal.append( component = "x11", @@ -715,6 +790,7 @@ class RuntimeSupervisorService : Service() { mapOf( "rootfs" to owned.rootfsName, "display" to DISPLAY_NUMBER, + "launch_ms" to SystemClock.elapsedRealtime() - desktopRequestedAtMs, ), ) monitorDesktop(owned) diff --git a/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt b/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt index 988d68d..f15f785 100644 --- a/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt +++ b/app/src/main/java/org/randomcoder/udroid/x11/GuestX11TransportProbe.kt @@ -1,6 +1,7 @@ package org.randomcoder.udroid.x11 import android.content.Context +import android.os.SystemClock import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive @@ -20,12 +21,16 @@ internal sealed interface GuestX11TransportResult { data class Ready( val protocolMajor: Int, val protocolMinor: Int, + val addressBytes: Int? = null, + val elapsedMs: Long? = null, ) : GuestX11TransportResult data class Failed( val stage: String, val errno: Int?, val detail: String, + val addressBytes: Int? = null, + val elapsedMs: Long? = null, ) : GuestX11TransportResult { val userMessage: String get() = @@ -37,23 +42,33 @@ internal sealed interface GuestX11TransportResult { } } +internal data class GuestX11ProbeReport( + val abstractSocket: GuestX11TransportResult, + val filesystemSocket: GuestX11TransportResult, + val client: GuestX11ClientProbeResult, +) + +internal data class GuestX11ClientProbeResult( + val status: String, + val exitCode: Int?, + val elapsedMs: Long, + val fields: Map, + val output: String, +) + +internal enum class GuestX11SocketNamespace { + ABSTRACT, + FILESYSTEM, +} + /** Verifies the display socket through the same PRoot alias used by Linux clients. */ internal class GuestX11TransportProbe(private val context: Context) { fun query( runtime: ProotRuntime, rootfs: File, endpoint: X11DisplayEndpoint, - ): GuestX11TransportResult { + ): GuestX11ProbeReport { val nativeProbe = NativeProbeInstaller.install(context) - val arguments = - GuestX11ProbeCommand.buildArguments( - prootPath = runtime.executable.absolutePath, - rootfsPath = ProotPathContract.rootfsPath(context, rootfs), - socketDirectory = endpoint.socketDirectory.absolutePath, - bindSocket = endpoint.requiresGuestBind, - nativeProbe = nativeProbe.absolutePath, - forceDenied = BuildConfig.X11_GUEST_PROBE_FAULT == "deny", - ) val temporaryDirectory = File(context.cacheDir, "proot").apply { check(mkdirs() || isDirectory) { "Could not prepare PRoot temporary storage" } @@ -68,36 +83,237 @@ internal class GuestX11TransportProbe(private val context: Context) { val separator = value.indexOf('=') value.substring(0, separator) to value.substring(separator + 1) } + fun startProbe(namespace: GuestX11SocketNamespace): Process { + val arguments = + GuestX11ProbeCommand.buildArguments( + prootPath = runtime.executable.absolutePath, + rootfsPath = ProotPathContract.rootfsPath(context, rootfs), + socketDirectory = endpoint.socketDirectory.absolutePath, + bindSocket = endpoint.requiresGuestBind, + nativeProbe = nativeProbe.absolutePath, + forceDenied = + BuildConfig.X11_GUEST_PROBE_FAULT == "deny" && + namespace == GuestX11SocketNamespace.FILESYSTEM, + socketNamespace = namespace, + ) + val command = + AndroidExecutableCommand.create( + runtime.executable, + *arguments.drop(1).toTypedArray(), + ) + return ProcessBuilder(command) + .directory(context.filesDir) + .redirectErrorStream(true) + .apply { + environment().clear() + environment().putAll(environment) + }.start() + } + fun awaitProbe( + process: Process, + namespace: GuestX11SocketNamespace, + ): GuestX11TransportResult { + if (!process.waitFor(PROBE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + process.destroyForcibly() + return GuestX11TransportResult.Failed( + stage = "timeout", + errno = null, + detail = "guest handshake timed out", + ) + } + val output = process.inputStream.bufferedReader().use { it.readText() } + return GuestX11ProbeOutput.parse(output, namespace) + } + + // Start both at once so a blocked abstract connect cannot delay or mask the + // filesystem transport that actually gates desktop startup. + val abstractProcess = startProbe(GuestX11SocketNamespace.ABSTRACT) + val filesystemProcess = startProbe(GuestX11SocketNamespace.FILESYSTEM) + return GuestX11ProbeReport( + abstractSocket = + awaitProbe( + abstractProcess, + GuestX11SocketNamespace.ABSTRACT, + ), + filesystemSocket = + awaitProbe( + filesystemProcess, + GuestX11SocketNamespace.FILESYSTEM, + ), + client = queryGuestClient(runtime, rootfs, endpoint, environment), + ) + } + + private fun queryGuestClient( + runtime: ProotRuntime, + rootfs: File, + endpoint: X11DisplayEndpoint, + hostEnvironment: Map, + ): GuestX11ClientProbeResult { + val guestHome = if (File(rootfs, "root").isDirectory) "/root" else "/" + val arguments = + GuestX11ClientProbeCommand.buildArguments( + prootPath = runtime.executable.absolutePath, + rootfsPath = ProotPathContract.rootfsPath(context, rootfs), + socketDirectory = endpoint.socketDirectory.absolutePath, + bindSocket = endpoint.requiresGuestBind, + guestHome = guestHome, + ) val command = AndroidExecutableCommand.create( runtime.executable, *arguments.drop(1).toTypedArray(), ) + val startedMs = SystemClock.elapsedRealtime() val process = ProcessBuilder(command) .directory(context.filesDir) .redirectErrorStream(true) .apply { environment().clear() - environment().putAll(environment) + environment().putAll(hostEnvironment) }.start() - if (!process.waitFor(PROBE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + if (!process.waitFor(CLIENT_PROBE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { process.destroyForcibly() - return GuestX11TransportResult.Failed( - stage = "timeout", - errno = null, - detail = "guest handshake timed out", + return GuestX11ClientProbeResult( + status = "timeout", + exitCode = null, + elapsedMs = SystemClock.elapsedRealtime() - startedMs, + fields = emptyMap(), + output = "guest xrdb probe timed out", ) } val output = process.inputStream.bufferedReader().use { it.readText() } - return GuestX11ProbeOutput.parse(output) + val fields = GuestX11ClientProbeOutput.parseFields(output) + val boundedOutput = + fields["xrdb_output"] + ?: output + .lineSequence() + .joinToString(" ") + .trim() + .take(CLIENT_PROBE_OUTPUT_CHARS) + return GuestX11ClientProbeResult( + status = fields["xrdb_status"] ?: "probe_output_missing", + exitCode = process.exitValue(), + elapsedMs = SystemClock.elapsedRealtime() - startedMs, + fields = fields, + output = boundedOutput, + ) } private companion object { const val PROBE_TIMEOUT_SECONDS = 3L + const val CLIENT_PROBE_TIMEOUT_SECONDS = 8L + const val CLIENT_PROBE_OUTPUT_CHARS = 1_024 } } +internal object GuestX11ClientProbeCommand { + private const val RECORD_PREFIX = "UDROID_X11|" + + fun buildArguments( + prootPath: String, + rootfsPath: String, + socketDirectory: String, + bindSocket: Boolean, + guestHome: String, + androidBindMounts: List = ANDROID_PROOT_BIND_MOUNTS, + ): List = + buildList { + add(prootPath) + add("--link2symlink") + add("--kill-on-exit") + add("--root-id") + add("--rootfs=$rootfsPath") + androidBindMounts.forEach { path -> + add("-b") + add(path) + } + if (bindSocket) { + add("-b") + add("$socketDirectory:/tmp/.X11-unix") + } + add("--cwd=/") + add("/usr/bin/env") + add("-i") + add("HOME=$guestHome") + add("USER=root") + add("LOGNAME=root") + add("SHELL=/bin/sh") + add("LANG=C.UTF-8") + add("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") + add("DISPLAY=:0") + add("/bin/sh") + add("-c") + add(probeScript()) + } + + private fun probeScript(): String = + """ + out=/tmp/.udroid-xrdb-probe.${'$'}${'$'} + trap 'rm -f "${'$'}out" "${'$'}out".*' EXIT HUP INT TERM + record() { printf '${RECORD_PREFIX}%s=%s\n' "${'$'}1" "${'$'}2"; } + record client_display "${'$'}DISPLAY" + record selinux_context "${'$'}(cat /proc/self/attr/current 2>/dev/null | tr '\n\r' ' ' | head -c 192)" + record selinux_enforcing "${'$'}(cat /sys/fs/selinux/enforce 2>/dev/null | head -c 8)" + record mount_namespace "${'$'}(readlink /proc/self/ns/mnt 2>/dev/null | head -c 96)" + record process_security "${'$'}(grep -E '^(NoNewPrivs|Seccomp):' /proc/self/status 2>/dev/null | tr '\n\r' ',,' | head -c 96)" + record socket_stat "${'$'}(stat -Lc '%F mode=%a uid=%u gid=%g size=%s' /tmp/.X11-unix/X0 2>&1 | tr '\n\r' ' ' | head -c 256)" + record socket_directory_stat "${'$'}(stat -Lc '%F mode=%a uid=%u gid=%g' /tmp/.X11-unix 2>&1 | tr '\n\r' ' ' | head -c 256)" + record xrdb_path "${'$'}(command -v xrdb 2>/dev/null | head -c 256)" + if command -v dpkg-query >/dev/null 2>&1; then + record libxcb_package "${'$'}(dpkg-query -W libxcb1 2>&1 | tr '\n\r\t' ' ' | head -c 256)" + elif command -v apk >/dev/null 2>&1; then + record libxcb_package "${'$'}(apk info -v libxcb 2>&1 | tr '\n\r' ' ' | head -c 256)" + else + record libxcb_package unavailable + fi + if command -v xrdb >/dev/null 2>&1; then + run_xrdb() { + label="${'$'}1" + target="${'$'}2" + probe_out="${'$'}out.${'$'}label" + if command -v timeout >/dev/null 2>&1; then + timeout 2 xrdb -display "${'$'}target" -query >"${'$'}probe_out" 2>&1 + else + xrdb -display "${'$'}target" -query >"${'$'}probe_out" 2>&1 + fi + rc=${'$'}? + record "${'$'}{label}_display" "${'$'}target" + record "${'$'}{label}_exit" "${'$'}rc" + if [ "${'$'}rc" -eq 0 ]; then + record "${'$'}{label}_status" ready + record "${'$'}{label}_output" '' + else + record "${'$'}{label}_status" failed + record "${'$'}{label}_output" "${'$'}(tr '\n\r' ' ' <"${'$'}probe_out" | head -c 512)" + fi + } + run_xrdb xrdb ':0' + run_xrdb xrdb_unix 'unix/:0' + run_xrdb xrdb_path '/tmp/.X11-unix/X0' + else + record xrdb_exit 127 + record xrdb_status unavailable + record xrdb_output 'xrdb is not installed' + fi + """.trimIndent() +} + +internal object GuestX11ClientProbeOutput { + private const val RECORD_PREFIX = "UDROID_X11|" + + fun parseFields(output: String): Map = + output + .lineSequence() + .filter { it.startsWith(RECORD_PREFIX) } + .mapNotNull { line -> + val value = line.removePrefix(RECORD_PREFIX) + val separator = value.indexOf('=') + if (separator <= 0) null else value.substring(0, separator) to value.substring(separator + 1) + }.toMap() +} + internal object GuestX11ProbeCommand { const val GUEST_SOCKET = "/tmp/.X11-unix/X0" const val GUEST_PROBE = "/tmp/.udroid-x11-probe" @@ -109,6 +325,7 @@ internal object GuestX11ProbeCommand { bindSocket: Boolean, nativeProbe: String, forceDenied: Boolean, + socketNamespace: GuestX11SocketNamespace = GuestX11SocketNamespace.FILESYSTEM, androidBindMounts: List = ANDROID_PROOT_BIND_MOUNTS, systemLinkerPath: String = AndroidExecutableCommand.systemLinkerPath(), ): List = @@ -131,13 +348,23 @@ internal object GuestX11ProbeCommand { add("--cwd=/") add(systemLinkerPath) add(GUEST_PROBE) - add(if (forceDenied) "--x11-deny" else "--x11") + add( + when { + forceDenied -> "--x11-deny" + socketNamespace == GuestX11SocketNamespace.ABSTRACT -> "--x11-abstract" + else -> "--x11" + }, + ) add(GUEST_SOCKET) } } internal object GuestX11ProbeOutput { - fun parse(output: String): GuestX11TransportResult { + fun parse( + output: String, + namespace: GuestX11SocketNamespace, + ): GuestX11TransportResult { + val namespaceValue = namespace.name.lowercase() val record = output .lineSequence() @@ -146,24 +373,36 @@ internal object GuestX11ProbeOutput { .mapNotNull { line -> runCatching { Json.parseToJsonElement(line).jsonObject }.getOrNull() }.firstOrNull { value -> - value["event"]?.jsonPrimitive?.content == "x11_guest_probe" + value["event"]?.jsonPrimitive?.content == "x11_guest_probe" && + value["socket_namespace"]?.jsonPrimitive?.content == namespaceValue } - ?: return GuestX11TransportResult.Failed( - stage = "probe_output", - errno = null, - detail = output.trim().takeIf(String::isNotEmpty) ?: "probe returned no result", - ) + val fallbackDetail = output.trim().takeIf(String::isNotEmpty) ?: "probe returned no result" + if (record == null) { + return GuestX11TransportResult.Failed( + stage = "probe_output", + errno = null, + detail = "$namespaceValue probe returned no result: $fallbackDetail", + ) + } + return parseRecord(record) + } + + private fun parseRecord(record: kotlinx.serialization.json.JsonObject): GuestX11TransportResult { val status = record["status"]?.jsonPrimitive?.content.orEmpty() if (status == "ready") { return GuestX11TransportResult.Ready( protocolMajor = record["protocol_major"]?.jsonPrimitive?.content?.toIntOrNull() ?: 0, protocolMinor = record["protocol_minor"]?.jsonPrimitive?.content?.toIntOrNull() ?: 0, + addressBytes = record["address_bytes"]?.jsonPrimitive?.content?.toIntOrNull(), + elapsedMs = record["elapsed_ms"]?.jsonPrimitive?.content?.toLongOrNull(), ) } return GuestX11TransportResult.Failed( stage = status.ifBlank { "unknown" }, errno = record["errno"]?.jsonPrimitive?.content?.toIntOrNull(), detail = record["detail"]?.jsonPrimitive?.content ?: status.ifBlank { "unknown failure" }, + addressBytes = record["address_bytes"]?.jsonPrimitive?.content?.toIntOrNull(), + elapsedMs = record["elapsed_ms"]?.jsonPrimitive?.content?.toLongOrNull(), ) } } diff --git a/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt b/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt index 5d65300..8c79ad5 100644 --- a/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/x11/GuestX11TransportProbeTest.kt @@ -9,17 +9,23 @@ class GuestX11TransportProbeTest { fun `ready output preserves negotiated protocol`() { val result = GuestX11ProbeOutput.parse( - """{"event":"x11_guest_probe","status":"ready","protocol_major":11,"protocol_minor":0}""", + """ + {"event":"x11_guest_probe","status":"ready","socket_namespace":"filesystem","protocol_major":11,"protocol_minor":0,"address_bytes":110,"elapsed_ms":7} + """.trimIndent(), + GuestX11SocketNamespace.FILESYSTEM, ) - assertEquals(GuestX11TransportResult.Ready(11, 0), result) + assertEquals(GuestX11TransportResult.Ready(11, 0, 110, 7), result) } @Test fun `permission denial becomes a transport-specific error`() { val result = GuestX11ProbeOutput.parse( - """{"event":"x11_guest_probe","status":"connect_failed","errno":13,"detail":"Permission denied"}""", + """ + {"event":"x11_guest_probe","status":"connect_failed","socket_namespace":"abstract","errno":13,"detail":"Permission denied"} + """.trimIndent(), + GuestX11SocketNamespace.ABSTRACT, ) as GuestX11TransportResult.Failed assertEquals("connect_failed", result.stage) @@ -54,6 +60,24 @@ class GuestX11TransportProbeTest { ) } + @Test + fun `abstract probe selects the Linux abstract namespace`() { + val arguments = + GuestX11ProbeCommand.buildArguments( + prootPath = "/data/proot", + rootfsPath = "/data/rootfs", + socketDirectory = "/data/x11/.X11-unix", + bindSocket = false, + nativeProbe = "/data/runtime_probe", + forceDenied = false, + socketNamespace = GuestX11SocketNamespace.ABSTRACT, + androidBindMounts = emptyList(), + systemLinkerPath = "/system/bin/linker64", + ) + + assertEquals("--x11-abstract", arguments[arguments.lastIndex - 1]) + } + @Test fun `fault build selects deterministic permission denial`() { val arguments = @@ -71,4 +95,42 @@ class GuestX11TransportProbeTest { assertEquals("--x11-deny", arguments[arguments.lastIndex - 1]) assertTrue("/data/x11/.X11-unix:/tmp/.X11-unix" !in arguments) } + + @Test + fun `guest client probe uses the desktop display and bound socket`() { + val arguments = + GuestX11ClientProbeCommand.buildArguments( + prootPath = "/data/proot", + rootfsPath = "/data/rootfs", + socketDirectory = "/data/x11/.X11-unix", + bindSocket = true, + guestHome = "/root", + androidBindMounts = listOf("/system"), + ) + + assertTrue("/data/x11/.X11-unix:/tmp/.X11-unix" in arguments) + assertTrue("DISPLAY=:0" in arguments) + assertTrue(arguments.last().contains("xrdb -display \"${'$'}target\" -query")) + assertTrue(arguments.last().contains("run_xrdb xrdb_unix 'unix/:0'")) + assertTrue(arguments.last().contains("run_xrdb xrdb_path '/tmp/.X11-unix/X0'")) + assertTrue(arguments.last().contains("/proc/self/attr/current")) + } + + @Test + fun `guest client output keeps only bounded diagnostic records`() { + val fields = + GuestX11ClientProbeOutput.parseFields( + """ + unrelated output + UDROID_X11|xrdb_status=failed + UDROID_X11|xrdb_exit=1 + UDROID_X11|xrdb_output=xrdb: Permission denied + """.trimIndent(), + ) + + assertEquals("failed", fields["xrdb_status"]) + assertEquals("1", fields["xrdb_exit"]) + assertEquals("xrdb: Permission denied", fields["xrdb_output"]) + assertEquals(3, fields.size) + } }