commit 11ea8025b0e82b762de405e3f551e50f829fac87 Author: Fr4nz D13trich Date: Sun Jan 4 20:10:16 2026 +0100 Repo cloned diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8382973 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +
+logo + +

APatch

+ +[![Latest Release](https://img.shields.io/github/v/release/bmax121/APatch?label=Release&logo=github)](https://github.com/bmax121/APatch/releases/latest) +[![Nightly Release](https://img.shields.io/badge/Nightly%20release-gray?logo=hackthebox&logoColor=fff)](https://nightly.link/bmax121/APatch/workflows/build/main/APatch) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/APatch) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/APatchGroup) +[![GitHub License](https://img.shields.io/github/license/bmax121/APatch?logo=gnu)](/LICENSE) + +
+ +The patching of Android kernel and Android system. + +- A new kernel-based root solution for Android devices. +- APM: Support for modules similar to Magisk. +- KPM: Support for modules that allow you to inject any code into the kernel (Provides kernel function `inline-hook` and `syscall-table-hook`). +- APatch relies on [KernelPatch](https://github.com/bmax121/KernelPatch/). +- The APatch UI and the APModule source code have been derived and modified from [KernelSU](https://github.com/tiann/KernelSU). + +[Get it on F-Droid](https://f-droid.org/packages/me.bmax.apatch/) + +Or download the latest APK from the [Releases Section](https://github.com/bmax121/APatch/releases/latest). + +## Supported Versions + +- Only supports the ARM64 architecture. +- Only supports Android kernel versions 3.18 - 6.12 + +Support for Samsung devices with security protection: Planned + +## Requirement + +Kernel configs: + +- `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y` + +- `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=n`: Initial support + +## Security Alert + +The **SuperKey** has higher privileges than root access. +Weak or compromised keys can lead to unauthorized control of your device. +It is critical to use robust keys and safeguard them from exposure to maintain the security of your device. + +## Translation + +To help translate APatch or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/apatch/). PR of APatch translation is no longer accepted, because it will conflict with Weblate. + +
+ +[![Translation Status](https://hosted.weblate.org/widget/APatch/open-graph.png)](https://hosted.weblate.org/engage/APatch/) + +
+ +## Get Help + +### Usage + +For usage, please refer to [our official documentation](https://apatch.dev). +It's worth noting that the documentation is currently not quite complete, and the content may change at any time. +Furthermore, we need more volunteers to [contribute to the documentation](https://github.com/AndroidPatch/APatchDocs) in other languages. + +### Updates + +- Telegram Channel: [@APatchUpdates](https://t.me/APatchChannel) + +### Discussions + +- Telegram Group: [@APatchDiscussions(EN/CN)](https://t.me/Apatch_discuss) +- Telegram Group: [中文](https://t.me/APatch_CN_Group) + +### More Information + +- [Documents](docs/) + +## Credits + +- [KernelPatch](https://github.com/bmax121/KernelPatch/): The core. +- [Magisk](https://github.com/topjohnwu/Magisk): magiskboot and magiskpolicy. +- [KernelSU](https://github.com/tiann/KernelSU): App UI, and Magisk module like support. + +## License + +APatch is licensed under the GNU General Public License v3 [GPL-3](http://www.gnu.org/copyleft/gpl.html). diff --git a/apd/.gitignore b/apd/.gitignore new file mode 100644 index 0000000..3c71873 --- /dev/null +++ b/apd/.gitignore @@ -0,0 +1,2 @@ +/target +.cargo/ \ No newline at end of file diff --git a/apd/Cargo.lock b/apd/Cargo.lock new file mode 100644 index 0000000..24a0935 --- /dev/null +++ b/apd/Cargo.lock @@ -0,0 +1,1766 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "apd" +version = "0.1.0" +dependencies = [ + "android-properties", + "android_logger", + "anyhow", + "clap", + "const_format", + "csv", + "derive-new", + "encoding_rs", + "env_logger", + "errno 0.3.14", + "extattr", + "getopts", + "is_executable", + "java-properties", + "jwalk", + "libc", + "log", + "loopdev", + "nom", + "notify", + "procfs", + "retry", + "rustix 0.38.34", + "serde", + "serde_json", + "signal-hook", + "walkdir", + "which", + "zip 5.1.1", + "zip-extensions", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "extattr" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b59f8a77817ff1b795adafc535941bdf664184f5f95e0b6d1d77dd6d12815dc" +dependencies = [ + "bitflags 1.3.2", + "errno 0.2.8", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.10.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "java-properties" +version = "2.0.0" +source = "git+https://github.com/AndroidPatch/java-properties.git?branch=master#42a4aa941b70ded2dd3be9e9f892471023e70229" +dependencies = [ + "encoding_rs", + "lazy_static", + "regex-lite", +] + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" +dependencies = [ + "crossbeam", + "rayon", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "liblzma" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73c36d08cad03a3fbe2c4e7bb3a9e84c57e4ee4135ed0b065cade3d98480c648" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "loopdev" +version = "0.5.0" +source = "git+https://github.com/AndroidPatch/loopdev#7a921f8d966477a645b1188732fac486c71a68ef" +dependencies = [ + "errno 0.2.8", + "libc", +] + +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags 2.10.0", + "chrono", + "flate2", + "hex", + "procfs-core", + "rustix 0.38.44", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.10.0", + "chrono", + "hex", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "retry" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e211f878258887b3e65dd3c8ff9f530fe109f441a117ee0cdc27f341355032" +dependencies = [ + "rand", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "git+https://github.com/AndroidPatch/rustix?branch=main#733001cedefc11944d363daab7b369a57b1c8403" +dependencies = [ + "bitflags 2.10.0", + "errno 0.3.14", + "itoa", + "libc", + "linux-raw-sys 0.4.15", + "once_cell", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno 0.3.14", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno 0.3.14", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +dependencies = [ + "env_home", + "rustix 1.1.2", + "winsafe", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "liblzma", + "memchr", + "zopfli", +] + +[[package]] +name = "zip" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" +dependencies = [ + "arbitrary", + "crc32fast", + "deflate64", + "flate2", + "indexmap", + "lzma-rust2", + "memchr", + "time", + "zopfli", +] + +[[package]] +name = "zip-extensions" +version = "0.8.3" +source = "git+https://github.com/AndroidPatch/zip-extensions-rs.git?branch=master#6ab02d515fee354d8f7922b6f087f62a0a0caf44" +dependencies = [ + "zip 4.6.1", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/apd/Cargo.toml b/apd/Cargo.toml new file mode 100644 index 0000000..4d030d9 --- /dev/null +++ b/apd/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "apd" +version = "0.1.0" +edition = "2024" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +csv = "1.3.1" +clap = { version = "4", features = ["derive"] } +const_format = "0.2" +zip = { version = "5.1.1",features = [ + "deflate", + "deflate64", + "time", + "lzma", + "xz", +], default-features = false } +zip-extensions = { git = "https://github.com/AndroidPatch/zip-extensions-rs.git", branch = "master", features = [ + "deflate", + "lzma", + "xz", +], default-features = false } +java-properties = { git = "https://github.com/AndroidPatch/java-properties.git", branch = "master", default-features = false } +log = "0.4" +env_logger = "0.11" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +encoding_rs = "0.8" +walkdir="2.4" +retry = "2" +libc = "0.2" +extattr = "1" +jwalk = "0.8" +is_executable = "1" +nom = "8" +derive-new = "0.7.0" +which = "8" +getopts = "0.2" +errno = "0.3.14" +notify = "8.2" +signal-hook = "0.3" + +[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] +rustix = { git = "https://github.com/AndroidPatch/rustix", branch = "main", features = ["all-apis"] } +# some android specific dependencies which compiles under unix are also listed here for convenience of coding +android-properties = { version = "0.2.2", features = ["bionic-deprecated"] } +procfs = "0.17" +loopdev = { git = "https://github.com/AndroidPatch/loopdev" } + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = { version = "0.15", default-features = false } + +[profile.release] +strip = true +overflow-checks = false +opt-level = 3 +codegen-units = 1 +panic = "abort" +lto = "fat" diff --git a/apd/build.rs b/apd/build.rs new file mode 100644 index 0000000..f55b700 --- /dev/null +++ b/apd/build.rs @@ -0,0 +1,62 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::process::Command; + +fn get_git_version() -> Result<(u32, String), std::io::Error> { + let output = Command::new("git") + .args(["rev-list", "--count", "HEAD"]) + .output()?; + + let output = output.stdout; + let version_code = String::from_utf8(output).expect("Failed to read git count stdout"); + let version_code: u32 = version_code + .trim() + .parse() + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse git count"))?; + let version_code = 10000 + 200 + version_code; // For historical reasons + + let version_name = String::from_utf8( + Command::new("git") + .args(["describe", "--tags", "--always"]) + .output()? + .stdout, + ) + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to read git describe stdout", + ) + })?; + let version_name = version_name.trim_start_matches('v').to_string(); + Ok((version_code, version_name)) +} + +fn main() { + // update VersionCode when git repository change + println!("cargo:rerun-if-changed=../.git/HEAD"); + println!("cargo:rerun-if-changed=../.git/refs/"); + + let (code, name) = match get_git_version() { + Ok((code, name)) => (code, name), + Err(_) => { + // show warning if git is not installed + println!("cargo:warning=Failed to get git version, using 0.0.0"); + (0, "0.0.0".to_string()) + } + }; + let out_dir = env::var("OUT_DIR").expect("Failed to get $OUT_DIR"); + println!("out_dir: ${out_dir}"); + println!("code: ${code}"); + let out_dir = Path::new(&out_dir); + File::create(Path::new(out_dir).join("VERSION_CODE")) + .expect("Failed to create VERSION_CODE") + .write_all(code.to_string().as_bytes()) + .expect("Failed to write VERSION_CODE"); + + File::create(Path::new(out_dir).join("VERSION_NAME")) + .expect("Failed to create VERSION_NAME") + .write_all(name.trim().as_bytes()) + .expect("Failed to write VERSION_NAME"); +} diff --git a/apd/src/apd.rs b/apd/src/apd.rs new file mode 100644 index 0000000..8246f07 --- /dev/null +++ b/apd/src/apd.rs @@ -0,0 +1,231 @@ +use anyhow::{Ok, Result}; + +#[cfg(unix)] +use getopts::Options; +use std::env; +#[cfg(unix)] +use std::os::unix::process::CommandExt; +use std::path::PathBuf; +use std::{ffi::CStr, process::Command}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use crate::pty::prepare_pty; +use crate::{ + defs, + utils::{self, umask}, +}; +use rustix::thread::{Gid, Uid, set_thread_res_gid, set_thread_res_uid}; + +fn print_usage(opts: Options) { + let brief = "APatch\n\nUsage: [options] [-] [user [argument...]]".to_string(); + print!("{}", opts.usage(&brief)); +} + +fn set_identity(uid: u32, gid: u32) { + #[cfg(any(target_os = "linux", target_os = "android"))] + let gid = unsafe { Gid::from_raw(gid) }; + let uid = unsafe { Uid::from_raw(uid) }; + set_thread_res_gid(gid, gid, gid).ok(); + set_thread_res_uid(uid, uid, uid).ok(); +} + +#[cfg(not(unix))] +pub fn root_shell() -> Result<()> { + unimplemented!() +} + +#[cfg(unix)] +pub fn root_shell() -> Result<()> { + // we are root now, this was set in kernel! + let env_args: Vec = env::args().collect(); + let args = env_args + .iter() + .position(|arg| arg == "-c") + .map(|i| { + let rest = env_args[i + 1..].to_vec(); + let mut new_args = env_args[..i].to_vec(); + new_args.push("-c".to_string()); + if !rest.is_empty() { + new_args.push(rest.join(" ")); + } + new_args + }) + .unwrap_or_else(|| env_args.clone()); + + let mut opts = Options::new(); + opts.optopt( + "c", + "command", + "pass COMMAND to the invoked shell", + "COMMAND", + ); + opts.optflag("h", "help", "display this help message and exit"); + opts.optflag("l", "login", "pretend the shell to be a login shell"); + opts.optflag( + "p", + "preserve-environment", + "preserve the entire environment", + ); + opts.optopt( + "s", + "shell", + "use SHELL instead of the default /system/bin/sh", + "SHELL", + ); + opts.optflag("v", "version", "display version number and exit"); + opts.optflag("V", "", "display version code and exit"); + opts.optflag( + "M", + "mount-master", + "force run in the global mount namespace", + ); + opts.optflag("", "no-pty", "Do not allocate a new pseudo terminal."); + + // Replace -cn with -z, -mm with -M for supporting getopt_long + let args = args + .into_iter() + .map(|e| { + if e == "-mm" { + "-M".to_string() + } else if e == "-cn" { + "-z".to_string() + } else { + e + } + }) + .collect::>(); + + let matches = match opts.parse(&args[1..]) { + Result::Ok(m) => m, + Err(f) => { + println!("{f}"); + print_usage(opts); + std::process::exit(-1); + } + }; + + if matches.opt_present("h") { + print_usage(opts); + return Ok(()); + } + + if matches.opt_present("v") { + println!("{}:APatch", defs::VERSION_NAME); + return Ok(()); + } + + if matches.opt_present("V") { + println!("{}", defs::VERSION_CODE); + return Ok(()); + } + + let shell = matches.opt_str("s").unwrap_or("/system/bin/sh".to_string()); + let mut is_login = matches.opt_present("l"); + let preserve_env = matches.opt_present("p"); + let mount_master = matches.opt_present("M"); + + // we've made sure that -c is the last option and it already contains the whole command, no need to construct it again + let args = matches + .opt_str("c") + .map(|cmd| vec!["-c".to_string(), cmd]) + .unwrap_or_default(); + + let mut free_idx = 0; + if !matches.free.is_empty() && matches.free[free_idx] == "-" { + is_login = true; + free_idx += 1; + } + + // use current uid if no user specified, these has been done in kernel! + let mut uid = unsafe { libc::getuid() }; + let gid = unsafe { libc::getgid() }; + if free_idx < matches.free.len() { + let name = &matches.free[free_idx]; + uid = unsafe { + #[cfg(target_arch = "aarch64")] + let pw = libc::getpwnam(name.as_ptr()).as_ref(); + #[cfg(target_arch = "x86_64")] + let pw = libc::getpwnam(name.as_ptr() as *const i8).as_ref(); + + match pw { + Some(pw) => pw.pw_uid, + None => name.parse::().unwrap_or(0), + } + } + } + + // https://github.com/topjohnwu/Magisk/blob/master/native/src/core/su/su_daemon.cpp#L408 + let arg0 = if is_login { "-" } else { &shell }; + + let mut command = &mut Command::new(&shell); + + if !preserve_env { + // This is actually incorrect, I don't know why. + // command = command.env_clear(); + + let pw = unsafe { libc::getpwuid(uid).as_ref() }; + + if let Some(pw) = pw { + let home = unsafe { CStr::from_ptr(pw.pw_dir) }; + let pw_name = unsafe { CStr::from_ptr(pw.pw_name) }; + + let home = home.to_string_lossy(); + let pw_name = pw_name.to_string_lossy(); + + command = command + .env("HOME", home.as_ref()) + .env("USER", pw_name.as_ref()) + .env("LOGNAME", pw_name.as_ref()) + .env("SHELL", &shell); + } + } + + // add /data/adb/ap/bin to PATH + #[cfg(any(target_os = "linux", target_os = "android"))] + add_path_to_env(defs::BINARY_DIR)?; + + // when AP_RC_PATH exists and ENV is not set, set ENV to AP_RC_PATH + if PathBuf::from(defs::AP_RC_PATH).exists() && env::var("ENV").is_err() { + command = command.env("ENV", defs::AP_RC_PATH); + } + #[cfg(target_os = "android")] + if !matches.opt_present("no-pty") { + if let Err(e) = prepare_pty() { + log::error!("failed to prepare pty: {:?}", e); + } + } + // escape from the current cgroup and become session leader + // WARNING!!! This cause some root shell hang forever! + // command = command.process_group(0); + command = unsafe { + command.pre_exec(move || { + umask(0o22); + utils::switch_cgroups(); + + // switch to global mount namespace + #[cfg(any(target_os = "linux", target_os = "android"))] + let global_namespace_enable = + std::fs::read_to_string(defs::GLOBAL_NAMESPACE_FILE).unwrap_or("0".to_string()); + if global_namespace_enable.trim() == "1" || mount_master { + let _ = utils::switch_mnt_ns(1); + } + + set_identity(uid, gid); + + Result::Ok(()) + }) + }; + + command = command.args(args).arg0(arg0); + Err(command.exec().into()) +} + +fn add_path_to_env(path: &str) -> Result<()> { + let mut paths = + env::var_os("PATH").map_or(Vec::new(), |val| env::split_paths(&val).collect::>()); + let new_path = PathBuf::from(path.trim_end_matches('/')); + paths.push(new_path); + let new_path_env = env::join_paths(paths)?; + unsafe { env::set_var("PATH", new_path_env) }; + Ok(()) +} diff --git a/apd/src/assets.rs b/apd/src/assets.rs new file mode 100644 index 0000000..1f5ab45 --- /dev/null +++ b/apd/src/assets.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use const_format::concatcp; + +use crate::{defs::BINARY_DIR, utils}; + +pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, "resetprop"); +pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, "busybox"); +pub const MAGISKPOLICY_PATH: &str = concatcp!(BINARY_DIR, "magiskpolicy"); + +pub fn ensure_binaries() -> Result<()> { + utils::ensure_binary(RESETPROP_PATH)?; + utils::ensure_binary(BUSYBOX_PATH)?; + utils::ensure_binary(MAGISKPOLICY_PATH)?; + Ok(()) +} diff --git a/apd/src/banner b/apd/src/banner new file mode 100644 index 0000000..578ce52 --- /dev/null +++ b/apd/src/banner @@ -0,0 +1,5 @@ + _ ____ _ _ + / \ | _ \ __ _| |_ ___| |__ + / _ \ | |_) / _` | __/ __| '_ \ + / ___ \| __/ (_| | || (__| | | | +/_/ \_\_| \__,_|\__\___|_| |_| diff --git a/apd/src/cli.rs b/apd/src/cli.rs new file mode 100644 index 0000000..a4f9e6e --- /dev/null +++ b/apd/src/cli.rs @@ -0,0 +1,162 @@ +use anyhow::Result; +use clap::Parser; + +#[cfg(target_os = "android")] +use android_logger::Config; +#[cfg(target_os = "android")] +use log::LevelFilter; + +use crate::{defs, event, module, supercall, utils}; + +/// APatch cli +#[derive(Parser, Debug)] +#[command(author, version = defs::VERSION_CODE, about, long_about = None)] +struct Args { + #[arg( + short, + long, + value_name = "KEY", + help = "Super key for authentication root" + )] + superkey: Option, + #[command(subcommand)] + command: Commands, +} + +#[derive(clap::Subcommand, Debug)] +enum Commands { + /// Manage APatch modules + Module { + #[command(subcommand)] + command: Module, + }, + + /// Trigger `post-fs-data` event + PostFsData, + + /// Trigger `service` event + Services, + + /// Trigger `boot-complete` event + BootCompleted, + + /// Start uid listener for synchronizing root list + UidListener, + + /// SELinux policy Patch tool + Sepolicy { + #[command(subcommand)] + command: Sepolicy, + }, +} + +#[derive(clap::Subcommand, Debug)] +enum Module { + /// Install module + Install { + /// module zip file path + zip: String, + }, + + /// Uninstall module + Uninstall { + /// module id + id: String, + }, + + /// enable module + Enable { + /// module id + id: String, + }, + + /// disable module + Disable { + // module id + id: String, + }, + + /// run action for module + Action { + // module id + id: String, + }, + + /// list all modules + List, +} + +#[derive(clap::Subcommand, Debug)] +enum Sepolicy { + /// Check if sepolicy statement is supported/valid + Check { + /// sepolicy statements + sepolicy: String, + }, +} + +pub fn run() -> Result<()> { + #[cfg(target_os = "android")] + android_logger::init_once( + Config::default() + .with_max_level(LevelFilter::Trace) // limit log level + .with_tag("APatchD") + .with_filter( + android_logger::FilterBuilder::new() + .filter_level(LevelFilter::Trace) + .filter_module("notify", LevelFilter::Warn) + .build(), + ), + ); + + #[cfg(not(target_os = "android"))] + env_logger::init(); + + // the kernel executes su with argv[0] = "/system/bin/kp" or "/system/bin/su" or "su" or "kp" and replace it with us + let arg0 = std::env::args().next().unwrap_or_default(); + if arg0.ends_with("kp") || arg0.ends_with("su") { + return crate::apd::root_shell(); + } + + let cli = Args::parse(); + + log::info!("command: {:?}", cli.command); + + if let Some(ref _superkey) = cli.superkey { + supercall::privilege_apd_profile(&cli.superkey); + } + + let result = match cli.command { + Commands::PostFsData => event::on_post_data_fs(cli.superkey), + + Commands::BootCompleted => event::on_boot_completed(cli.superkey), + + Commands::UidListener => event::start_uid_listener(), + + Commands::Module { command } => { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + utils::switch_mnt_ns(1)?; + } + match command { + Module::Install { zip } => module::install_module(&zip), + Module::Uninstall { id } => module::uninstall_module(&id), + Module::Action { id } => module::run_action(&id), + Module::Enable { id } => module::enable_module(&id), + Module::Disable { id } => module::disable_module(&id), + Module::List => module::list_modules(), + } + } + + Commands::Sepolicy { command } => match command { + Sepolicy::Check { sepolicy } => crate::sepolicy::check_rule(&sepolicy), + }, + + Commands::Services => event::on_services(cli.superkey), + }; + + if let Err(e) = &result { + log::error!("Error: {:?}", e); + } + result +} diff --git a/apd/src/defs.rs b/apd/src/defs.rs new file mode 100644 index 0000000..abef27d --- /dev/null +++ b/apd/src/defs.rs @@ -0,0 +1,36 @@ +use const_format::concatcp; + +pub const ADB_DIR: &str = "/data/adb/"; +pub const WORKING_DIR: &str = concatcp!(ADB_DIR, "ap/"); +pub const BINARY_DIR: &str = concatcp!(WORKING_DIR, "bin/"); +pub const APATCH_LOG_FOLDER: &str = concatcp!(WORKING_DIR, "log/"); + +pub const AP_RC_PATH: &str = concatcp!(WORKING_DIR, ".aprc"); +pub const GLOBAL_NAMESPACE_FILE: &str = concatcp!(ADB_DIR, ".global_namespace_enable"); +pub const LITEMODE_FILE: &str = concatcp!(ADB_DIR, ".litemode_enable"); +pub const FORCE_OVERLAYFS_FILE: &str = concatcp!(ADB_DIR, ".overlayfs_enable"); +pub const AP_OVERLAY_SOURCE: &str = "APatch"; +pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "apd"); + +pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/"); +pub const MODULE_UPDATE_TMP_IMG: &str = concatcp!(WORKING_DIR, "update_tmp.img"); + +// warning: this directory should not change, or you need to change the code in module_installer.sh!!! +pub const MODULE_UPDATE_TMP_DIR: &str = concatcp!(ADB_DIR, "modules_update/"); +pub const MODULE_MOUNT_DIR: &str = concatcp!(ADB_DIR, "modules_mount/"); + +pub const SYSTEM_RW_DIR: &str = concatcp!(MODULE_DIR, ".rw/"); + +pub const TEMP_DIR: &str = "/debug_ramdisk"; +pub const TEMP_DIR_LEGACY: &str = "/sbin"; + +pub const MODULE_WEB_DIR: &str = "webroot"; +pub const MODULE_ACTION_SH: &str = "action.sh"; +pub const DISABLE_FILE_NAME: &str = "disable"; +pub const UPDATE_FILE_NAME: &str = "update"; +pub const REMOVE_FILE_NAME: &str = "remove"; +pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; +pub const PTS_NAME: &str = "pts"; + +pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE")); +pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME")); diff --git a/apd/src/event.rs b/apd/src/event.rs new file mode 100644 index 0000000..4576170 --- /dev/null +++ b/apd/src/event.rs @@ -0,0 +1,606 @@ +use crate::magic_mount; +use crate::module; +use crate::supercall::fork_for_result; +use crate::utils::{ensure_dir_exists, ensure_file_exists, get_work_dir, switch_cgroups}; +use crate::{ + assets, defs, mount, restorecon, supercall, + supercall::{init_load_package_uid_config, init_load_su_path, refresh_ap_package_list}, + utils::{self, ensure_clean_dir}, +}; +use anyhow::{Context, Result, bail, ensure}; +use extattr::{Flags as XattrFlags, lgetxattr, lsetxattr}; +use libc::SIGPWR; +use log::{info, warn}; +use notify::event::{ModifyKind, RenameMode}; +use notify::{Config, Event, EventKind, INotifyWatcher, RecursiveMode, Watcher}; +use rustix::mount::*; +use signal_hook::consts::signal::*; +use signal_hook::iterator::Signals; +use std::ffi::CStr; +use std::fs::{remove_dir_all, rename}; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::{collections::HashMap, thread}; +use std::{env, fs, io}; +use walkdir::WalkDir; + +fn copy_with_xattr(src: &Path, dest: &Path) -> io::Result<()> { + fs::copy(src, dest)?; + + if let Ok(xattr_value) = lgetxattr(src, "security.selinux") { + lsetxattr(dest, "security.selinux", &xattr_value, XattrFlags::empty())?; + } + + Ok(()) +} + +fn copy_dir_with_xattr(src: &Path, dest: &Path) -> io::Result<()> { + for entry in WalkDir::new(src) { + let entry = entry?; + let rel_path = entry + .path() + .strip_prefix(src) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let target_path = dest.join(rel_path); + if entry.file_type().is_dir() { + fs::create_dir_all(&target_path)?; + } else if entry.file_type().is_file() { + copy_with_xattr(entry.path(), &target_path)?; + } + } + Ok(()) +} + +fn mount_partition(partition_name: &str, lowerdir: &Vec) -> Result<()> { + if lowerdir.is_empty() { + warn!("partition: {partition_name} lowerdir is empty"); + return Ok(()); + } + + let partition = format!("/{partition_name}"); + + // if /partition is a symlink and linked to /system/partition, then we don't need to overlay it separately + if Path::new(&partition).read_link().is_ok() { + warn!("partition: {partition} is a symlink"); + return Ok(()); + } + + let mut workdir = None; + let mut upperdir = None; + let system_rw_dir = Path::new(defs::SYSTEM_RW_DIR); + if system_rw_dir.exists() { + workdir = Some(system_rw_dir.join(partition_name).join("workdir")); + upperdir = Some(system_rw_dir.join(partition_name).join("upperdir")); + } + + mount::mount_overlay(&partition, lowerdir, workdir, upperdir) +} + +pub fn mount_systemlessly(module_dir: &str, is_img: bool) -> Result<()> { + // construct overlay mount params + if !is_img { + info!("fallback to modules.img"); + let module_update_dir = defs::MODULE_DIR; + let module_dir = defs::MODULE_MOUNT_DIR; + let tmp_module_img = defs::MODULE_UPDATE_TMP_IMG; + let tmp_module_path = Path::new(tmp_module_img); + + ensure_clean_dir(module_dir)?; + info!("- Preparing image"); + + let module_update_flag = Path::new(defs::WORKING_DIR).join(defs::UPDATE_FILE_NAME); + + if !tmp_module_path.exists() { + ensure_file_exists(&module_update_flag)?; + } + + if module_update_flag.exists() { + if tmp_module_path.exists() { + //if it has update, remove tmp file + fs::remove_file(tmp_module_path)?; + } + let total_size = calculate_total_size(Path::new(module_update_dir))?; //create modules adapt size + info!( + "Total size of files in '{}': {} bytes", + tmp_module_path.display(), + total_size + ); + let grow_size = 128 * 1024 * 1024 + total_size; + fs::File::create(tmp_module_img) + .context("Failed to create ext4 image file")? + .set_len(grow_size) + .context("Failed to extend ext4 image")?; + let result = Command::new("mkfs.ext4") + .arg("-b") + .arg("1024") + .arg(tmp_module_img) + .stdout(std::process::Stdio::piped()) + .output()?; + ensure!( + result.status.success(), + "Failed to format ext4 image: {}", + String::from_utf8(result.stderr)? + ); + info!("Checking Image"); + module::check_image(tmp_module_img)?; + } + info!("- Mounting image"); + mount::AutoMountExt4::try_new(tmp_module_img, module_dir, false) + .with_context(|| "mount module image failed".to_string())?; + info!("mounted {} to {}", tmp_module_img, module_dir); + let _ = restorecon::setsyscon(module_dir); + if module_update_flag.exists() { + let command_string = format!( + "cp --preserve=context -RP {}* {};", + module_update_dir, module_dir + ); + let args = vec!["-c", &command_string]; + let _ = utils::run_command("sh", &args, None)?.wait()?; + } + mount_systemlessly(module_dir, true)?; + return Ok(()); + } + let module_dir_origin = Path::new(defs::MODULE_DIR); + let dir = fs::read_dir(module_dir); + let Ok(dir) = dir else { + bail!("open {} failed", defs::MODULE_DIR); + }; + + let mut system_lowerdir: Vec = Vec::new(); + + let partition = vec!["vendor", "product", "system_ext", "odm", "oem"]; + let mut partition_lowerdir: HashMap> = HashMap::new(); + for ele in &partition { + partition_lowerdir.insert((*ele).to_string(), Vec::new()); + } + + for entry in dir.flatten() { + let module = entry.path(); + if !module.is_dir() { + continue; + } + if let Some(module_name) = module.file_name() { + let real_module_path = module_dir_origin.join(module_name); + + let disabled = real_module_path.join(defs::DISABLE_FILE_NAME).exists(); + + if disabled { + info!("module: {} is disabled, ignore!", module.display()); + continue; + } + } + + let skip_mount = module.join(defs::SKIP_MOUNT_FILE_NAME).exists(); + if skip_mount { + info!("module: {} skip_mount exist, skip!", module.display()); + continue; + } + + let module_system = Path::new(&module).join("system"); + if module_system.is_dir() { + system_lowerdir.push(format!("{}", module_system.display())); + } + + for part in &partition { + // if /partition is a mountpoint, we would move it to $MODPATH/$partition when install + // otherwise it must be a symlink and we don't need to overlay! + let part_path = Path::new(&module).join(part); + if part_path.is_dir() { + if let Some(v) = partition_lowerdir.get_mut(*part) { + v.push(format!("{}", part_path.display())); + } + } + } + } + + // mount /system first + if let Err(e) = mount_partition("system", &system_lowerdir) { + warn!("mount system failed: {:#}", e); + } + + // mount other partitions + for (k, v) in partition_lowerdir { + if let Err(e) = mount_partition(&k, &v) { + warn!("mount {k} failed: {:#}", e); + } + } + + Ok(()) +} + +pub fn systemless_bind_mount(_module_dir: &str) -> Result<()> { + // call magisk mount + magic_mount::magic_mount()?; + Ok(()) +} + +pub fn calculate_total_size(path: &Path) -> io::Result { + let mut total_size = 0; + if path.is_dir() { + for entry in fs::read_dir(path)? { + let entry = entry?; + let file_type = entry.file_type()?; + if file_type.is_file() { + total_size += entry.metadata()?.len(); + } else if file_type.is_dir() { + total_size += calculate_total_size(&entry.path())?; + } + } + } + Ok(total_size) +} +pub fn move_file(module_update_dir: &str, module_dir: &str) -> Result<()> { + for entry in fs::read_dir(module_update_dir)? { + let entry = entry?; + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + if entry.path().is_dir() { + let source_path = Path::new(module_update_dir).join(file_name_str.as_ref()); + let target_path = Path::new(module_dir).join(file_name_str.as_ref()); + let update = target_path.join(defs::UPDATE_FILE_NAME).exists(); + if update { + if target_path.exists() { + info!( + "Removing existing folder in target directory: {}", + file_name_str + ); + remove_dir_all(&target_path)?; + } + + info!("Moving {} to target directory", file_name_str); + rename(&source_path, &target_path)?; + } + } + } + Ok(()) +} +pub fn on_post_data_fs(superkey: Option) -> Result<()> { + utils::umask(0); + use std::process::Stdio; + #[cfg(unix)] + init_load_package_uid_config(&superkey); + + init_load_su_path(&superkey); + + let args = ["/data/adb/ap/bin/magiskpolicy", "--magisk", "--live"]; + fork_for_result("/data/adb/ap/bin/magiskpolicy", &args, &superkey); + + info!("Re-privilege apd profile after injecting sepolicy"); + supercall::privilege_apd_profile(&superkey); + + if utils::has_magisk() { + warn!("Magisk detected, skip post-fs-data!"); + return Ok(()); + } + + // Create log environment + if !Path::new(defs::APATCH_LOG_FOLDER).exists() { + fs::create_dir(defs::APATCH_LOG_FOLDER).expect("Failed to create log folder"); + let permissions = fs::Permissions::from_mode(0o700); + fs::set_permissions(defs::APATCH_LOG_FOLDER, permissions) + .expect("Failed to set permissions"); + } + let command_string = format!( + "rm -rf {}*.old.log; for file in {}*; do mv \"$file\" \"$file.old.log\"; done", + defs::APATCH_LOG_FOLDER, + defs::APATCH_LOG_FOLDER + ); + let mut args = vec!["-c", &command_string]; + // for all file to .old + let result = utils::run_command("sh", &args, None)?.wait()?; + if result.success() { + info!("Successfully deleted .old files."); + } else { + info!("Failed to delete .old files."); + } + let logcat_path = format!("{}locat.log", defs::APATCH_LOG_FOLDER); + let dmesg_path = format!("{}dmesg.log", defs::APATCH_LOG_FOLDER); + let bootlog = fs::File::create(dmesg_path)?; + args = vec![ + "-s", + "9", + "120s", + "logcat", + "-b", + "main,system,crash", + "-f", + &logcat_path, + "logcatcher-bootlog:S", + "&", + ]; + let _ = unsafe { + Command::new("timeout") + .process_group(0) + .pre_exec(|| { + switch_cgroups(); + Ok(()) + }) + .args(args) + .spawn() + }; + args = vec!["-s", "9", "120s", "dmesg", "-w"]; + let _result = unsafe { + Command::new("timeout") + .process_group(0) + .pre_exec(|| { + switch_cgroups(); + Ok(()) + }) + .args(args) + .stdout(Stdio::from(bootlog)) + .spawn() + }; + + let key = "KERNELPATCH_VERSION"; + match env::var(key) { + Ok(value) => println!("{}: {}", key, value), + Err(_) => println!("{} not found", key), + } + + let key = "KERNEL_VERSION"; + match env::var(key) { + Ok(value) => println!("{}: {}", key, value), + Err(_) => println!("{} not found", key), + } + + let safe_mode = utils::is_safe_mode(superkey.clone()); + + if safe_mode { + // we should still mount modules.img to `/data/adb/modules` in safe mode + // becuase we may need to operate the module dir in safe mode + warn!("safe mode, skip common post-fs-data.d scripts"); + if let Err(e) = module::disable_all_modules() { + warn!("disable all modules failed: {}", e); + } + } else { + // Then exec common post-fs-data scripts + if let Err(e) = module::exec_common_scripts("post-fs-data.d", true) { + warn!("exec common post-fs-data scripts failed: {}", e); + } + } + let module_update_dir = defs::MODULE_UPDATE_TMP_DIR; //save module place + let module_dir = defs::MODULE_DIR; // run modules place + let module_update_flag = Path::new(defs::WORKING_DIR).join(defs::UPDATE_FILE_NAME); // if update ,there will be renewed modules file + assets::ensure_binaries().with_context(|| "binary missing")?; + + if Path::new(defs::MODULE_UPDATE_TMP_DIR).exists() { + move_file(module_update_dir, module_dir)?; + fs::remove_dir_all(module_update_dir)?; + } + let is_lite_mode_enabled = Path::new(defs::LITEMODE_FILE).exists(); + + if safe_mode { + warn!("safe mode, skip post-fs-data scripts and disable all modules!"); + if let Err(e) = module::disable_all_modules() { + warn!("disable all modules failed: {}", e); + } + return Ok(()); + } + + if let Err(e) = module::prune_modules() { + warn!("prune modules failed: {}", e); + } + + if let Err(e) = restorecon::restorecon() { + warn!("restorecon failed: {}", e); + } + + // load sepolicy.rule + if module::load_sepolicy_rule().is_err() { + warn!("load sepolicy.rule failed"); + } + if is_lite_mode_enabled { + info!("litemode runing skip mount tempfs") + } else { + if let Err(e) = mount::mount_tmpfs(utils::get_tmp_path()) { + warn!("do temp dir mount failed: {}", e); + } + } + + // exec modules post-fs-data scripts + // TODO: Add timeout + if let Err(e) = module::exec_stage_script("post-fs-data", true) { + warn!("exec post-fs-data scripts failed: {}", e); + } + + // load system.prop + if let Err(e) = module::load_system_prop() { + warn!("load system.prop failed: {}", e); + } + + if utils::should_use_overlayfs()? { + // mount module systemlessly by overlay + let work_dir = get_work_dir(); + let tmp_dir = PathBuf::from(work_dir.clone()); + ensure_dir_exists(&tmp_dir)?; + mount( + defs::AP_OVERLAY_SOURCE, + &tmp_dir, + "tmpfs", + MountFlags::empty(), + "", + ) + .context("mount tmp")?; + mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?; + let dir_names = vec!["vendor", "product", "system_ext", "odm", "oem", "system"]; + let dir = fs::read_dir(module_dir)?; + for entry in dir.flatten() { + let module_path = entry.path(); + let disabled = module_path.join(defs::DISABLE_FILE_NAME).exists(); + if disabled { + info!("module: {} is disabled, ignore!", module_path.display()); + continue; + } + if module_path.is_dir() { + let module_name = module_path.file_name().unwrap().to_string_lossy(); + let module_dest = Path::new(&work_dir).join(module_name.as_ref()); + + for sub_dir in dir_names.iter() { + let sub_dir_path = module_path.join(sub_dir); + if sub_dir_path.exists() && sub_dir_path.is_dir() { + let sub_dir_dest = module_dest.join(sub_dir); + fs::create_dir_all(&sub_dir_dest)?; + + copy_dir_with_xattr(&sub_dir_path, &sub_dir_dest)?; + } + } + } + } + if let Err(e) = mount_systemlessly(&get_work_dir(), false) { + warn!("do systemless mount failed: {}", e); + } + if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) { + log::error!("failed to unmount tmp {}", e); + } + } else { + if !is_lite_mode_enabled { + if let Err(e) = systemless_bind_mount(module_dir) { + warn!("do systemless bind_mount failed: {}", e); + } + } else { + info!("litemode runing skip magic mount"); + } + } + + info!("remove update flag"); + let _ = fs::remove_file(module_update_flag); + + + + run_stage("post-mount", superkey, true); + + env::set_current_dir("/").with_context(|| "failed to chdir to /")?; + + Ok(()) +} + +fn run_stage(stage: &str, superkey: Option, block: bool) { + utils::umask(0); + + if utils::has_magisk() { + warn!("Magisk detected, skip {stage}"); + return; + } + + if utils::is_safe_mode(superkey) { + warn!("safe mode, skip {stage} scripts"); + if let Err(e) = module::disable_all_modules() { + warn!("disable all modules failed: {}", e); + } + return; + } + + if let Err(e) = module::exec_common_scripts(&format!("{stage}.d"), block) { + warn!("Failed to exec common {stage} scripts: {e}"); + } + if let Err(e) = module::exec_stage_script(stage, block) { + warn!("Failed to exec {stage} scripts: {e}"); + } +} + +pub fn on_services(superkey: Option) -> Result<()> { + info!("on_services triggered!"); + run_stage("service", superkey, false); + + Ok(()) +} + +fn run_uid_monitor() { + info!("Trigger run_uid_monitor!"); + + let mut command = &mut Command::new("/data/adb/apd"); + { + command = command.process_group(0); + command = unsafe { + command.pre_exec(|| { + // ignore the error? + switch_cgroups(); + Ok(()) + }) + }; + } + command = command.arg("uid-listener"); + + command + .spawn() + .map(|_| ()) + .expect("[run_uid_monitor] Failed to run uid monitor"); +} + +pub fn on_boot_completed(superkey: Option) -> Result<()> { + info!("on_boot_completed triggered!"); + + run_stage("boot-completed", superkey, false); + + run_uid_monitor(); + Ok(()) +} + +pub fn start_uid_listener() -> Result<()> { + info!("start_uid_listener triggered!"); + println!("[start_uid_listener] Registering..."); + + // create inotify instance + const SYS_PACKAGES_LIST_TMP: &str = "/data/system/packages.list.tmp"; + let sys_packages_list_tmp = PathBuf::from(&SYS_PACKAGES_LIST_TMP); + let dir: PathBuf = sys_packages_list_tmp.parent().unwrap().into(); + + let (tx, rx) = std::sync::mpsc::channel(); + let tx_clone = tx.clone(); + let mutex = Arc::new(Mutex::new(())); + + { + let mutex_clone = mutex.clone(); + thread::spawn(move || { + let mut signals = Signals::new(&[SIGTERM, SIGINT, SIGPWR]).unwrap(); + for sig in signals.forever() { + log::warn!("[shutdown] Caught signal {sig}, refreshing package list..."); + let skey = CStr::from_bytes_with_nul(b"su\0") + .expect("[shutdown_listener] CStr::from_bytes_with_nul failed"); + refresh_ap_package_list(&skey, &mutex_clone); + break; // 执行一次后退出线程 + } + }); + } + + let mut watcher = INotifyWatcher::new( + move |ev: notify::Result| match ev { + Ok(Event { + kind: EventKind::Modify(ModifyKind::Name(RenameMode::Both)), + paths, + .. + }) => { + if paths.contains(&sys_packages_list_tmp) { + info!("[uid_monitor] System packages list changed, sending to tx..."); + tx_clone.send(false).unwrap() + } + } + Err(err) => warn!("inotify error: {err}"), + _ => (), + }, + Config::default(), + )?; + + watcher.watch(dir.as_ref(), RecursiveMode::NonRecursive)?; + + let mut debounce = false; + while let Ok(delayed) = rx.recv() { + if delayed { + debounce = false; + let skey = CStr::from_bytes_with_nul(b"su\0") + .expect("[start_uid_listener] CStr::from_bytes_with_nul failed"); + refresh_ap_package_list(&skey, &mutex); + } else if !debounce { + thread::sleep(Duration::from_secs(1)); + debounce = true; + tx.send(true)?; + } + } + + Ok(()) +} diff --git a/apd/src/installer.sh b/apd/src/installer.sh new file mode 100644 index 0000000..c006e89 --- /dev/null +++ b/apd/src/installer.sh @@ -0,0 +1,441 @@ +#!/system/bin/sh +############################################ +# APatch Module installer script +# mostly from module_installer.sh +# and util_functions.sh in Magisk +############################################ + +umask 022 + +ui_print() { + if $BOOTMODE; then + echo "$1" + else + echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD + fi +} + +toupper() { + echo "$@" | tr '[:lower:]' '[:upper:]' +} + +grep_cmdline() { + local REGEX="s/^$1=//p" + { echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \ + sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \ + } 2>/dev/null | sed -n "$REGEX" +} + +grep_prop() { + local REGEX="s/^$1=//p" + shift + local FILES=$@ + [ -z "$FILES" ] && FILES='/system/build.prop' + cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1 +} + +grep_get_prop() { + local result=$(grep_prop $@) + if [ -z "$result" ]; then + # Fallback to getprop + getprop "$1" + else + echo $result + fi +} + +is_mounted() { + grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null + return $? +} + +abort() { + ui_print "$1" + $BOOTMODE || recovery_cleanup + [ ! -z $MODPATH ] && rm -rf $MODPATH + rm -rf $TMPDIR + exit 1 +} + +print_title() { + local len line1len line2len bar + line1len=$(echo -n $1 | wc -c) + line2len=$(echo -n $2 | wc -c) + len=$line2len + [ $line1len -gt $line2len ] && len=$line1len + len=$((len + 2)) + bar=$(printf "%${len}s" | tr ' ' '*') + ui_print "$bar" + ui_print " $1 " + [ "$2" ] && ui_print " $2 " + ui_print "$bar" +} + +###################### +# Environment Related +###################### + +setup_flashable() { + ensure_bb + $BOOTMODE && return + if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then + # We will have to manually find out OUTFD + for FD in `ls /proc/$$/fd`; do + if readlink /proc/$$/fd/$FD | grep -q pipe; then + if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then + OUTFD=$FD + break + fi + fi + done + fi + recovery_actions +} + +ensure_bb() { + : +} + +recovery_actions() { + : +} + +recovery_cleanup() { + : +} + +####################### +# Installation Related +####################### + +# find_block [partname...] +find_block() { + local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT + for BLOCK in "$@"; do + DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null + if [ ! -z $DEVICE ]; then + readlink -f $DEVICE + return 0 + fi + done + # Fallback by parsing sysfs uevents + for UEVENT in /sys/dev/block/*/uevent; do + DEVNAME=`grep_prop DEVNAME $UEVENT` + PARTNAME=`grep_prop PARTNAME $UEVENT` + for BLOCK in "$@"; do + if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then + echo /dev/block/$DEVNAME + return 0 + fi + done + done + # Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links + for DEV in "$@"; do + DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null + if [ ! -z $DEVICE ]; then + readlink -f $DEVICE + return 0 + fi + done + return 1 +} + +# setup_mntpoint +setup_mntpoint() { + local POINT=$1 + [ -L $POINT ] && mv -f $POINT ${POINT}_link + if [ ! -d $POINT ]; then + rm -f $POINT + mkdir -p $POINT + fi +} + +# mount_name +mount_name() { + local PART=$1 + local POINT=$2 + local FLAG=$3 + setup_mntpoint $POINT + is_mounted $POINT && return + # First try mounting with fstab + mount $FLAG $POINT 2>/dev/null + if ! is_mounted $POINT; then + local BLOCK=$(find_block $PART) + mount $FLAG $BLOCK $POINT || return + fi + ui_print "- Mounting $POINT" +} + +# mount_ro_ensure +mount_ro_ensure() { + # We handle ro partitions only in recovery + $BOOTMODE && return + local PART=$1 + local POINT=$2 + mount_name "$PART" $POINT '-o ro' + is_mounted $POINT || abort "! Cannot mount $POINT" +} + +mount_partitions() { + # Check A/B slot + SLOT=`grep_cmdline androidboot.slot_suffix` + if [ -z $SLOT ]; then + SLOT=`grep_cmdline androidboot.slot` + [ -z $SLOT ] || SLOT=_${SLOT} + fi + [ -z $SLOT ] || ui_print "- Current boot slot: $SLOT" + + # Mount ro partitions + if is_mounted /system_root; then + umount /system 2&>/dev/null + umount /system_root 2&>/dev/null + fi + mount_ro_ensure "system$SLOT app$SLOT" /system + if [ -f /system/init -o -L /system/init ]; then + SYSTEM_ROOT=true + setup_mntpoint /system_root + if ! mount --move /system /system_root; then + umount /system + umount -l /system 2>/dev/null + mount_ro_ensure "system$SLOT app$SLOT" /system_root + fi + mount -o bind /system_root/system /system + else + SYSTEM_ROOT=false + grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true + fi + # /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails + [ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro' + $SYSTEM_ROOT && ui_print "- Device is system-as-root" + + # Mount sepolicy rules dir locations in recovery (best effort) + if ! $BOOTMODE; then + mount_name "cache cac" /cache + mount_name metadata /metadata + mount_name persist /persist + fi +} + +api_level_arch_detect() { + API=$(grep_get_prop ro.build.version.sdk) + ABI=$(grep_get_prop ro.product.cpu.abi) + if [ "$ABI" = "x86" ]; then + ARCH=x86 + ABI32=x86 + IS64BIT=false + elif [ "$ABI" = "arm64-v8a" ]; then + ARCH=arm64 + ABI32=armeabi-v7a + IS64BIT=true + elif [ "$ABI" = "x86_64" ]; then + ARCH=x64 + ABI32=x86 + IS64BIT=true + else + ARCH=arm + ABI=armeabi-v7a + ABI32=armeabi-v7a + IS64BIT=false + fi +} + +################# +# Module Related +################# + +set_perm() { + chown $2:$3 $1 || return 1 + chmod $4 $1 || return 1 + local CON=$5 + [ -z $CON ] && CON=u:object_r:system_file:s0 + chcon $CON $1 || return 1 +} + +set_perm_recursive() { + find $1 -type d 2>/dev/null | while read dir; do + set_perm $dir $2 $3 $4 $6 + done + find $1 -type f -o -type l 2>/dev/null | while read file; do + set_perm $file $2 $3 $5 $6 + done +} + +mktouch() { + mkdir -p ${1%/*} 2>/dev/null + [ -z $2 ] && touch $1 || echo $2 > $1 + chmod 644 $1 +} + +mark_remove() { + mkdir -p ${1%/*} 2>/dev/null + mknod $1 c 0 0 + chmod 644 $1 +} + +mark_replace() { + # REPLACE must be directory!!! + # https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories + mkdir -p $1 2>/dev/null + setfattr -n trusted.overlay.opaque -v y $1 + chmod 644 $1 +} + +request_size_check() { + reqSizeM=`du -ms "$1" | cut -f1` +} + +request_zip_size_check() { + reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'` +} + +boot_actions() { return; } + +# Require ZIPFILE to be set +is_legacy_script() { + unzip -l "$ZIPFILE" install.sh | grep -q install.sh + return $? +} + +handle_partition() { + # if /system/vendor is a symlink, we need to move it out of $MODPATH/system, otherwise it will be overlayed + # if /system/vendor is a normal directory, it is ok to overlay it and we don't need to overlay it separately. + if [ ! -e $MODPATH/system/$1 ]; then + # no partition found + return; + fi + + if [ -L "/system/$1" ] && [ "$(readlink -f /system/$1)" = "/$1" ]; then + ui_print "- Handle partition /$1" + # we create a symlink if module want to access $MODPATH/system/$1 + # but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly) + mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1 + fi +} + +# Require OUTFD, ZIPFILE to be set +install_module() { + rm -rf $TMPDIR + mkdir -p $TMPDIR + chcon u:object_r:system_file:s0 $TMPDIR + cd $TMPDIR + + mount_partitions + api_level_arch_detect + + # Setup busybox and binaries + if $BOOTMODE; then + boot_actions + else + recovery_actions + fi + + # Extract prop file + unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2 + [ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!" + + local MODDIRNAME=modules + $BOOTMODE && MODDIRNAME=modules_update + local MODULEROOT=$NVBASE/$MODDIRNAME + MODID=`grep_prop id $TMPDIR/module.prop` + MODNAME=`grep_prop name $TMPDIR/module.prop` + MODAUTH=`grep_prop author $TMPDIR/module.prop` + MODPATH=$MODULEROOT/$MODID + + # Create mod paths + rm -rf $MODPATH + mkdir -p $MODPATH + + if is_legacy_script; then + unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2 + + # Load install script + . $TMPDIR/install.sh + + # Callbacks + print_modname + on_install + + [ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh + $SKIPMOUNT && touch $MODPATH/skip_mount + $PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop + cp -af $TMPDIR/module.prop $MODPATH/module.prop + $POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh + $LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh + + ui_print "- Setting permissions" + set_permissions + else + print_title "$MODNAME" "by $MODAUTH" + print_title "Powered by APatch" + + unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2 + + if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then + ui_print "- Extracting module files" + unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2 + + # Default permissions + set_perm_recursive $MODPATH 0 0 0755 0644 + set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755 + set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755 + set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755 + set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0 + fi + + # Load customization script + [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh + fi + + # Handle replace folders + for TARGET in $REPLACE; do + ui_print "- Replace target: $TARGET" + mark_replace $MODPATH$TARGET + done + + # Handle remove files + for TARGET in $REMOVE; do + ui_print "- Remove target: $TARGET" + mark_remove $MODPATH$TARGET + done + + handle_partition vendor + handle_partition system_ext + handle_partition product + + if $BOOTMODE; then + mktouch $NVBASE/modules/$MODID/update + rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null + rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null + cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop + fi + + # Remove stuff that doesn't belong to modules and clean up any empty directories + rm -rf \ + $MODPATH/system/placeholder $MODPATH/customize.sh \ + $MODPATH/README.md $MODPATH/.git* + rmdir -p $MODPATH 2>/dev/null + + cd / + $BOOTMODE || recovery_cleanup + rm -rf $TMPDIR + + ui_print "- Done" +} + +########## +# Presets +########## + +# Detect whether in boot mode +[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true +[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true +[ -z $BOOTMODE ] && BOOTMODE=false + +NVBASE=/data/adb +TMPDIR=/dev/tmp +POSTFSDATAD=$NVBASE/post-fs-data.d +SERVICED=$NVBASE/service.d + +# Some modules dependents on this +export MAGISK_VER=27.0 +export MAGISK_VER_CODE=27000 diff --git a/apd/src/installer_bind.sh b/apd/src/installer_bind.sh new file mode 100644 index 0000000..af5bad1 --- /dev/null +++ b/apd/src/installer_bind.sh @@ -0,0 +1,445 @@ +#!/system/bin/sh +############################################ +# APatch Module installer script +# mostly from module_installer.sh +# and util_functions.sh in Magisk +############################################ + +umask 022 + +ui_print() { + if $BOOTMODE; then + echo "$1" + else + echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD + fi +} + +toupper() { + echo "$@" | tr '[:lower:]' '[:upper:]' +} + +grep_cmdline() { + local REGEX="s/^$1=//p" + { echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \ + sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \ + } 2>/dev/null | sed -n "$REGEX" +} + +grep_prop() { + local REGEX="s/$1=//p" + shift + local FILES=$@ + [ -z "$FILES" ] && FILES='/system/build.prop' + cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1 | xargs +} + +grep_get_prop() { + local result=$(grep_prop $@) + if [ -z "$result" ]; then + # Fallback to getprop + getprop "$1" + else + echo $result + fi +} + +is_mounted() { + grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null + return $? +} + +abort() { + ui_print "$1" + $BOOTMODE || recovery_cleanup + [ ! -z $MODPATH ] && rm -rf $MODPATH + rm -rf $TMPDIR + exit 1 +} + +print_title() { + local len line1len line2len bar + line1len=$(echo -n $1 | wc -c) + line2len=$(echo -n $2 | wc -c) + len=$line2len + [ $line1len -gt $line2len ] && len=$line1len + len=$((len + 2)) + bar=$(printf "%${len}s" | tr ' ' '*') + ui_print "$bar" + ui_print " $1 " + [ "$2" ] && ui_print " $2 " + ui_print "$bar" +} + +check_sepolicy() { + /data/adb/apd sepolicy check "$1" + return $? +} + +###################### +# Environment Related +###################### + +setup_flashable() { + ensure_bb + $BOOTMODE && return + if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then + # We will have to manually find out OUTFD + for FD in /proc/$$/fd/*; do + if readlink /proc/$$/fd/$FD | grep -q pipe; then + if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then + OUTFD=$FD + break + fi + fi + done + fi + recovery_actions +} + +ensure_bb() { + : +} + +recovery_actions() { + : +} + +recovery_cleanup() { + : +} + +####################### +# Installation Related +####################### + +# find_block [partname...] +find_block() { + local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT + for BLOCK in "$@"; do + DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null + if [ ! -z $DEVICE ]; then + readlink -f $DEVICE + return 0 + fi + done + # Fallback by parsing sysfs uevents + for UEVENT in /sys/dev/block/*/uevent; do + DEVNAME=`grep_prop DEVNAME $UEVENT` + PARTNAME=`grep_prop PARTNAME $UEVENT` + for BLOCK in "$@"; do + if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then + echo /dev/block/$DEVNAME + return 0 + fi + done + done + # Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links + for DEV in "$@"; do + DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null + if [ ! -z $DEVICE ]; then + readlink -f $DEVICE + return 0 + fi + done + return 1 +} + +# setup_mntpoint +setup_mntpoint() { + local POINT=$1 + [ -L $POINT ] && mv -f $POINT ${POINT}_link + if [ ! -d $POINT ]; then + rm -f $POINT + mkdir -p $POINT + fi +} + +# mount_name +mount_name() { + local PART=$1 + local POINT=$2 + local FLAG=$3 + setup_mntpoint $POINT + is_mounted $POINT && return + # First try mounting with fstab + mount $FLAG $POINT 2>/dev/null + if ! is_mounted $POINT; then + local BLOCK=$(find_block $PART) + mount $FLAG $BLOCK $POINT || return + fi + ui_print "- Mounting $POINT" +} + +# mount_ro_ensure +mount_ro_ensure() { + # We handle ro partitions only in recovery + $BOOTMODE && return + local PART=$1 + local POINT=$2 + mount_name "$PART" $POINT '-o ro' + is_mounted $POINT || abort "! Cannot mount $POINT" +} + +mount_partitions() { + # Check A/B slot + SLOT=`grep_cmdline androidboot.slot_suffix` + if [ -z $SLOT ]; then + SLOT=`grep_cmdline androidboot.slot` + [ -z $SLOT ] || SLOT=_${SLOT} + fi + [ -z $SLOT ] || ui_print "- Current boot slot: $SLOT" + + # Mount ro partitions + if is_mounted /system_root; then + umount /system 2&>/dev/null + umount /system_root 2&>/dev/null + fi + mount_ro_ensure "system$SLOT app$SLOT" /system + if [ -f /system/init -o -L /system/init ]; then + SYSTEM_ROOT=true + setup_mntpoint /system_root + if ! mount --move /system /system_root; then + umount /system + umount -l /system 2>/dev/null + mount_ro_ensure "system$SLOT app$SLOT" /system_root + fi + mount -o bind /system_root/system /system + else + SYSTEM_ROOT=false + grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true + fi + # /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails + [ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro' + $SYSTEM_ROOT && ui_print "- Device is system-as-root" + + # Mount sepolicy rules dir locations in recovery (best effort) + if ! $BOOTMODE; then + mount_name "cache cac" /cache + mount_name metadata /metadata + mount_name persist /persist + fi +} + +api_level_arch_detect() { + API=$(grep_get_prop ro.build.version.sdk) + ABI=$(grep_get_prop ro.product.cpu.abi) + if [ "$ABI" = "x86" ]; then + ARCH=x86 + ABI32=x86 + IS64BIT=false + elif [ "$ABI" = "arm64-v8a" ]; then + ARCH=arm64 + ABI32=armeabi-v7a + IS64BIT=true + elif [ "$ABI" = "x86_64" ]; then + ARCH=x64 + ABI32=x86 + IS64BIT=true + else + ARCH=arm + ABI=armeabi-v7a + ABI32=armeabi-v7a + IS64BIT=false + fi +} + +################# +# Module Related +################# + +set_perm() { + chown $2:$3 $1 || return 1 + chmod $4 $1 || return 1 + local CON=$5 + [ -z $CON ] && CON=u:object_r:system_file:s0 + chcon $CON $1 || return 1 +} + +set_perm_recursive() { + find $1 -type d 2>/dev/null | while read dir; do + set_perm $dir $2 $3 $4 $6 + done + find $1 -type f -o -type l 2>/dev/null | while read file; do + set_perm $file $2 $3 $5 $6 + done +} + +mktouch() { + mkdir -p ${1%/*} 2>/dev/null + [ -z $2 ] && touch $1 || echo $2 > $1 + chmod 644 $1 +} + +mark_remove() { + mkdir -p ${1%/*} 2>/dev/null + mknod $1 c 0 0 + chmod 644 $1 +} + +mark_replace() { + # REPLACE must be directory!!! + # https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories + mkdir -p $1 2>/dev/null + setfattr -n trusted.overlay.opaque -v y $1 + chmod 644 $1 +} + +request_size_check() { + reqSizeM=`du -ms "$1" | cut -f1` +} + +request_zip_size_check() { + reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'` +} + +boot_actions() { return; } + +# Require ZIPFILE to be set +is_legacy_script() { + unzip -l "$ZIPFILE" install.sh | grep -q install.sh + return $? +} + +handle_partition() { + PARTITION="$1" + REQUIRE_SYMLINK="$2" + if [ ! -e "$MODPATH/system/$PARTITION" ]; then + # no partition found + return; + fi + + if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then + ui_print "- Handle partition /$PARTITION" + ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION" + fi +} + +# Require OUTFD, ZIPFILE to be set +install_module() { + rm -rf $TMPDIR + mkdir -p $TMPDIR + chcon u:object_r:system_file:s0 $TMPDIR + cd $TMPDIR + + mount_partitions + api_level_arch_detect + + # Setup busybox and binaries + if $BOOTMODE; then + boot_actions + else + recovery_actions + fi + + # Extract prop file + unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2 + [ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!" + + local MODDIRNAME=modules + $BOOTMODE && MODDIRNAME=modules_update + local MODULEROOT=$NVBASE/$MODDIRNAME + MODID=`grep_prop id $TMPDIR/module.prop` + MODNAME=`grep_prop name $TMPDIR/module.prop` + MODAUTH=`grep_prop author $TMPDIR/module.prop` + MODPATH=$MODULEROOT/$MODID + + # Create mod paths + rm -rf $MODPATH + mkdir -p $MODPATH + + if is_legacy_script; then + unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2 + + # Load install script + . $TMPDIR/install.sh + + # Callbacks + print_modname + on_install + + [ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh + $SKIPMOUNT && touch $MODPATH/skip_mount + $PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop + cp -af $TMPDIR/module.prop $MODPATH/module.prop + $POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh + $LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh + + ui_print "- Setting permissions" + set_permissions + else + print_title "$MODNAME" "by $MODAUTH" + print_title "Powered by APatch" + + unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2 + + if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then + ui_print "- Extracting module files" + unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2 + + # Default permissions + set_perm_recursive $MODPATH 0 0 0755 0644 + set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755 + set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755 + set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755 + set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0 + fi + + # Load customization script + [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh + fi + + handle_partition vendor true + handle_partition system_ext true + handle_partition product true + handle_partition odm false + + # Handle replace folders + for TARGET in $REPLACE; do + ui_print "- Replace target: $TARGET" + mark_replace "$MODPATH$TARGET" + done + + # Handle remove files + for TARGET in $REMOVE; do + ui_print "- Remove target: $TARGET" + mark_remove "$MODPATH$TARGET" + done + + if $BOOTMODE; then + mktouch $NVBASE/modules/$MODID/update + rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null + rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null + cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop + fi + + # Remove stuff that doesn't belong to modules and clean up any empty directories + rm -rf \ + $MODPATH/system/placeholder $MODPATH/customize.sh \ + $MODPATH/README.md $MODPATH/.git* + rmdir -p $MODPATH 2>/dev/null + + cd / + $BOOTMODE || recovery_cleanup + rm -rf $TMPDIR + + ui_print "- Done" +} + +########## +# Presets +########## + +# Detect whether in boot mode +[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true +[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true +[ -z $BOOTMODE ] && BOOTMODE=false + +NVBASE=/data/adb +TMPDIR=/dev/tmp +POSTFSDATAD=$NVBASE/post-fs-data.d +SERVICED=$NVBASE/service.d + +# Some modules dependents on this +export MAGISK_VER=27.0 +export MAGISK_VER_CODE=27000 \ No newline at end of file diff --git a/apd/src/magic_mount.rs b/apd/src/magic_mount.rs new file mode 100644 index 0000000..fc4c3f0 --- /dev/null +++ b/apd/src/magic_mount.rs @@ -0,0 +1,444 @@ +use crate::defs::{AP_OVERLAY_SOURCE, DISABLE_FILE_NAME, MODULE_DIR, SKIP_MOUNT_FILE_NAME}; +use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout}; +use crate::restorecon::{lgetfilecon, lsetfilecon}; +use crate::utils::ensure_dir_exists; +use crate::utils::get_work_dir; +use anyhow::{Context, Result, bail}; +use extattr::lgetxattr; +use rustix::fs::{ + Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount, + chmod, chown, mount, move_mount, unmount, +}; +use rustix::mount::mount_change; +use rustix::path::Arg; +use std::cmp::PartialEq; +use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::fs; +use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link}; +use std::os::unix::fs::{FileTypeExt, symlink}; +use std::path::{Path, PathBuf}; + +const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque"; + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +enum NodeFileType { + RegularFile, + Directory, + Symlink, + Whiteout, +} + +impl NodeFileType { + fn from_file_type(file_type: FileType) -> Option { + if file_type.is_file() { + Some(RegularFile) + } else if file_type.is_dir() { + Some(Directory) + } else if file_type.is_symlink() { + Some(Symlink) + } else { + None + } + } +} + +#[derive(Debug)] +struct Node { + name: String, + file_type: NodeFileType, + children: HashMap, + // the module that owned this node + module_path: Option, + replace: bool, + skip: bool, +} + +impl Node { + fn collect_module_files>(&mut self, module_dir: T) -> Result { + let dir = module_dir.as_ref(); + let mut has_file = false; + for entry in dir.read_dir()?.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + + let node = match self.children.entry(name.clone()) { + Entry::Occupied(o) => Some(o.into_mut()), + Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)), + }; + + if let Some(node) = node { + has_file |= if node.file_type == Directory { + node.collect_module_files(dir.join(&node.name))? || node.replace + } else { + true + } + } + } + + Ok(has_file) + } + + fn new_root(name: T) -> Self { + Node { + name: name.to_string(), + file_type: Directory, + children: Default::default(), + module_path: None, + replace: false, + skip: false, + } + } + + fn new_module(name: T, entry: &DirEntry) -> Option { + if let Ok(metadata) = entry.metadata() { + let path = entry.path(); + let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 { + Some(Whiteout) + } else { + NodeFileType::from_file_type(metadata.file_type()) + }; + if let Some(file_type) = file_type { + let mut replace = false; + if file_type == Directory { + if let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) { + if String::from_utf8_lossy(&v) == "y" { + replace = true; + } + } + } + return Some(Node { + name: name.to_string(), + file_type, + children: Default::default(), + module_path: Some(path), + replace, + skip: false, + }); + } + } + + None + } +} + +fn collect_module_files() -> Result> { + let mut root = Node::new_root(""); + let mut system = Node::new_root("system"); + let module_root = Path::new(MODULE_DIR); + let mut has_file = false; + for entry in module_root.read_dir()?.flatten() { + if !entry.file_type()?.is_dir() { + continue; + } + + if entry.path().join(DISABLE_FILE_NAME).exists() + || entry.path().join(SKIP_MOUNT_FILE_NAME).exists() + { + continue; + } + + let mod_system = entry.path().join("system"); + if !mod_system.is_dir() { + continue; + } + + log::debug!("collecting {}", entry.path().display()); + + has_file |= system.collect_module_files(&mod_system)?; + } + + if has_file { + for (partition, require_symlink) in [ + ("vendor", true), + ("system_ext", true), + ("product", true), + ("odm", false), + ("oem", false), + ] { + let path_of_root = Path::new("/").join(partition); + let path_of_system = Path::new("/system").join(partition); + if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) { + let name = partition.to_string(); + if let Some(node) = system.children.remove(&name) { + root.children.insert(name, node); + } + } + } + root.children.insert("system".to_string(), system); + Ok(Some(root)) + } else { + Ok(None) + } +} + +fn clone_symlink, Dst: AsRef>(src: Src, dst: Dst) -> Result<()> { + let src_symlink = read_link(src.as_ref())?; + symlink(&src_symlink, dst.as_ref())?; + lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?; + log::debug!( + "clone symlink {} -> {}({})", + dst.as_ref().display(), + dst.as_ref().display(), + src_symlink.display() + ); + Ok(()) +} + +fn mount_mirror, WP: AsRef>( + path: P, + work_dir_path: WP, + entry: &DirEntry, +) -> Result<()> { + let path = path.as_ref().join(entry.file_name()); + let work_dir_path = work_dir_path.as_ref().join(entry.file_name()); + let file_type = entry.file_type()?; + + if file_type.is_file() { + log::debug!( + "mount mirror file {} -> {}", + path.display(), + work_dir_path.display() + ); + fs::File::create(&work_dir_path)?; + bind_mount(&path, &work_dir_path)?; + } else if file_type.is_dir() { + log::debug!( + "mount mirror dir {} -> {}", + path.display(), + work_dir_path.display() + ); + create_dir(&work_dir_path)?; + let metadata = entry.metadata()?; + chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; + unsafe { + chown( + &work_dir_path, + Some(Uid::from_raw(metadata.uid())), + Some(Gid::from_raw(metadata.gid())), + )?; + } + lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?; + for entry in read_dir(&path)?.flatten() { + mount_mirror(&path, &work_dir_path, &entry)?; + } + } else if file_type.is_symlink() { + log::debug!( + "create mirror symlink {} -> {}", + path.display(), + work_dir_path.display() + ); + clone_symlink(&path, &work_dir_path)?; + } + + Ok(()) +} + +fn do_magic_mount, WP: AsRef>( + path: P, + work_dir_path: WP, + current: Node, + has_tmpfs: bool, +) -> Result<()> { + let mut current = current; + let path = path.as_ref().join(¤t.name); + let work_dir_path = work_dir_path.as_ref().join(¤t.name); + match current.file_type { + RegularFile => { + let target_path = if has_tmpfs { + fs::File::create(&work_dir_path)?; + &work_dir_path + } else { + &path + }; + if let Some(module_path) = ¤t.module_path { + log::debug!( + "mount module file {} -> {}", + module_path.display(), + work_dir_path.display() + ); + bind_mount(module_path, target_path)?; + } else { + bail!("cannot mount root file {}!", path.display()); + } + } + Symlink => { + if let Some(module_path) = ¤t.module_path { + log::debug!( + "create module symlink {} -> {}", + module_path.display(), + work_dir_path.display() + ); + clone_symlink(module_path, &work_dir_path)?; + } else { + bail!("cannot mount root symlink {}!", path.display()); + } + } + Directory => { + let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some(); + if !has_tmpfs && !create_tmpfs { + for it in &mut current.children { + let (name, node) = it; + let real_path = path.join(name); + let need = match node.file_type { + Symlink => true, + Whiteout => real_path.exists(), + _ => { + if let Ok(metadata) = real_path.symlink_metadata() { + let file_type = NodeFileType::from_file_type(metadata.file_type()) + .unwrap_or(Whiteout); + file_type != node.file_type || file_type == Symlink + } else { + // real path not exists + true + } + } + }; + if need { + if current.module_path.is_none() { + log::error!( + "cannot create tmpfs on {}, ignore: {name}", + path.display() + ); + node.skip = true; + continue; + } + create_tmpfs = true; + break; + } + } + } + + let has_tmpfs = has_tmpfs || create_tmpfs; + + if has_tmpfs { + log::debug!( + "creating tmpfs skeleton for {} at {}", + path.display(), + work_dir_path.display() + ); + create_dir_all(&work_dir_path)?; + let (metadata, path) = if path.exists() { + (path.metadata()?, &path) + } else if let Some(module_path) = ¤t.module_path { + (module_path.metadata()?, module_path) + } else { + bail!("cannot mount root dir {}!", path.display()); + }; + chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; + unsafe { + chown( + &work_dir_path, + Some(Uid::from_raw(metadata.uid())), + Some(Gid::from_raw(metadata.gid())), + )?; + } + lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?; + } + + if create_tmpfs { + log::debug!( + "creating tmpfs for {} at {}", + path.display(), + work_dir_path.display() + ); + bind_mount(&work_dir_path, &work_dir_path).context("bind self")?; + } + + if path.exists() && !current.replace { + for entry in path.read_dir()?.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + let result = if let Some(node) = current.children.remove(&name) { + if node.skip { + continue; + } + do_magic_mount(&path, &work_dir_path, node, has_tmpfs) + .with_context(|| format!("magic mount {}/{name}", path.display())) + } else if has_tmpfs { + mount_mirror(&path, &work_dir_path, &entry) + .with_context(|| format!("mount mirror {}/{name}", path.display())) + } else { + Ok(()) + }; + + if let Err(e) = result { + if has_tmpfs { + return Err(e); + } else { + log::error!("mount child {}/{name} failed: {}", path.display(), e); + } + } + } + } + + if current.replace { + if current.module_path.is_none() { + bail!( + "dir {} is declared as replaced but it is root!", + path.display() + ); + } else { + log::debug!("dir {} is replaced", path.display()); + } + } + + for (name, node) in current.children.into_iter() { + if node.skip { + continue; + } + if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs) + .with_context(|| format!("magic mount {}/{name}", path.display())) + { + if has_tmpfs { + return Err(e); + } else { + log::error!("mount child {}/{name} failed: {}", path.display(), e); + } + } + } + + if create_tmpfs { + log::debug!( + "moving tmpfs {} -> {}", + work_dir_path.display(), + path.display() + ); + move_mount(&work_dir_path, &path).context("move self")?; + mount_change(&path, MountPropagationFlags::PRIVATE).context("make self private")?; + } + } + Whiteout => { + log::debug!("file {} is removed", path.display()); + } + } + + Ok(()) +} + +pub fn magic_mount() -> Result<()> { + match collect_module_files()? { + Some(root) => { + log::debug!("collected: {:#?}", root); + let tmp_dir = PathBuf::from(get_work_dir()); + ensure_dir_exists(&tmp_dir)?; + mount( + AP_OVERLAY_SOURCE, + &tmp_dir, + "tmpfs", + MountFlags::empty(), + "", + ) + .context("mount tmp")?; + mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?; + let result = do_magic_mount("/", &tmp_dir, root, false); + if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) { + log::error!("failed to unmount tmp {}", e); + } + fs::remove_dir(tmp_dir).ok(); + result + } + _ => { + log::info!("no modules to mount, skipping!"); + Ok(()) + } + } +} diff --git a/apd/src/main.rs b/apd/src/main.rs new file mode 100644 index 0000000..6feedd3 --- /dev/null +++ b/apd/src/main.rs @@ -0,0 +1,18 @@ +mod apd; +mod assets; +mod cli; +mod defs; +mod event; +mod magic_mount; +mod module; +mod mount; +mod package; +#[cfg(any(target_os = "linux", target_os = "android"))] +mod pty; +mod restorecon; +mod sepolicy; +mod supercall; +mod utils; +fn main() -> anyhow::Result<()> { + cli::run() +} diff --git a/apd/src/module.rs b/apd/src/module.rs new file mode 100644 index 0000000..cfe0960 --- /dev/null +++ b/apd/src/module.rs @@ -0,0 +1,565 @@ +#[allow(clippy::wildcard_imports)] +use crate::utils::*; +use crate::{assets, defs, restorecon}; +use anyhow::{Context, Result, anyhow, bail, ensure}; +use const_format::concatcp; +use is_executable::is_executable; +use java_properties::PropertiesIter; +use log::{info, warn}; +use std::{ + collections::HashMap, + env::var as env_var, + fs, + io::Cursor, + path::{Path, PathBuf}, + process::{Command, Stdio}, + str::FromStr, +}; +use zip_extensions::zip_extract_file_to_memory; + +#[cfg(unix)] +use std::os::unix::{prelude::PermissionsExt, process::CommandExt}; + +const INSTALLER_CONTENT: &str = include_str!("./installer.sh"); +const INSTALLER_CONTENT_: &str = include_str!("./installer_bind.sh"); +const INSTALL_MODULE_SCRIPT: &str = concatcp!( + INSTALLER_CONTENT, + "\n", + "install_module", + "\n", + "exit 0", + "\n" +); +const INSTALL_MODULE_SCRIPT_: &str = concatcp!( + INSTALLER_CONTENT_, + "\n", + "install_module", + "\n", + "exit 0", + "\n" +); + +fn exec_install_script(module_file: &str) -> Result<()> { + let realpath = + fs::canonicalize(module_file).with_context(|| format!("realpath: {module_file} failed"))?; + + let content; + + if !should_use_overlayfs()? { + content = INSTALL_MODULE_SCRIPT_.to_string(); + } else { + content = INSTALL_MODULE_SCRIPT.to_string(); + } + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", "-c", &content]) + .env("ASH_STANDALONE", "1") + .env( + "PATH", + format!( + "{}:{}", + env_var("PATH").unwrap(), + defs::BINARY_DIR.trim_end_matches('/') + ), + ) + .env("APATCH", "true") + .env("APATCH_VER", defs::VERSION_NAME) + .env("APATCH_VER_CODE", defs::VERSION_CODE) + .env("APATCH_BIND_MOUNT", format!("{}", !should_use_overlayfs()?)) + .env("OUTFD", "1") + .env("ZIPFILE", realpath) + .status()?; + ensure!(result.success(), "Failed to install module script"); + Ok(()) +} + +// becuase we use something like A-B update +// we need to update the module state after the boot_completed +// if someone(such as the module) install a module before the boot_completed +// then it may cause some problems, just forbid it +fn ensure_boot_completed() -> Result<()> { + // ensure getprop sys.boot_completed == 1 + if getprop("sys.boot_completed").as_deref() != Some("1") { + bail!("Android is Booting!"); + } + Ok(()) +} + +fn mark_update() -> Result<()> { + ensure_file_exists(concatcp!(defs::WORKING_DIR, defs::UPDATE_FILE_NAME)) +} + +fn mark_module_state(module: &str, flag_file: &str, create_or_delete: bool) -> Result<()> { + let module_state_file = Path::new(defs::MODULE_DIR).join(module).join(flag_file); + if create_or_delete { + ensure_file_exists(module_state_file) + } else { + if module_state_file.exists() { + fs::remove_file(module_state_file)?; + } + Ok(()) + } +} + +fn foreach_module(active_only: bool, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> { + let modules_dir = Path::new(defs::MODULE_DIR); + let dir = fs::read_dir(modules_dir)?; + for entry in dir.flatten() { + let path = entry.path(); + if !path.is_dir() { + warn!("{} is not a directory, skip", path.display()); + continue; + } + + if active_only && path.join(defs::DISABLE_FILE_NAME).exists() { + info!("{} is disabled, skip", path.display()); + continue; + } + if active_only && path.join(defs::REMOVE_FILE_NAME).exists() { + warn!("{} is removed, skip", path.display()); + continue; + } + + f(&path)?; + } + + Ok(()) +} + +fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> { + foreach_module(true, f) +} + +pub fn check_image(img: &str) -> Result<()> { + let result = Command::new("e2fsck") + .args(["-yf", img]) + .stdout(Stdio::piped()) + .status() + .with_context(|| format!("Failed to exec e2fsck {img}"))?; + let code = result.code(); + // 0 or 1 is ok + // 0: no error + // 1: file system errors corrected + // https://man7.org/linux/man-pages/man8/e2fsck.8.html + // ensure!( + // code == Some(0) || code == Some(1), + // "Failed to check image, e2fsck exit code: {}", + // code.unwrap_or(-1) + // ); + info!("e2fsck exit code: {}", code.unwrap_or(-1)); + Ok(()) +} + +pub fn load_sepolicy_rule() -> Result<()> { + foreach_active_module(|path| { + let rule_file = path.join("sepolicy.rule"); + if !rule_file.exists() { + return Ok(()); + } + + info!("load policy: {}", &rule_file.display()); + Command::new(assets::MAGISKPOLICY_PATH) + .arg("--live") + .arg("--apply") + .arg(&rule_file) + .status() + .with_context(|| format!("Failed to exec {}", rule_file.display()))?; + Ok(()) + })?; + + Ok(()) +} + +fn exec_script>(path: T, wait: bool) -> Result<()> { + info!("exec {}", path.as_ref().display()); + + let mut command = &mut Command::new(assets::BUSYBOX_PATH); + #[cfg(unix)] + { + command = command.process_group(0); + command = unsafe { + command.pre_exec(|| { + // ignore the error? + switch_cgroups(); + Ok(()) + }) + }; + } + command = command + .current_dir(path.as_ref().parent().unwrap()) + .arg("sh") + .arg(path.as_ref()) + .env("ASH_STANDALONE", "1") + .env("APATCH", "true") + .env("APATCH_VER", defs::VERSION_NAME) + .env("APATCH_VER_CODE", defs::VERSION_CODE) + .env("APATCH_BIND_MOUNT", format!("{}", !should_use_overlayfs()?)) + .env( + "PATH", + format!( + "{}:{}", + env_var("PATH")?, + defs::BINARY_DIR.trim_end_matches('/') + ), + ); + + let result = if wait { + command.status().map(|_| ()) + } else { + command.spawn().map(|_| ()) + }; + result.map_err(|err| anyhow!("Failed to exec {}: {}", path.as_ref().display(), err)) +} + +pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> { + foreach_active_module(|module| { + let script_path = module.join(format!("{stage}.sh")); + if !script_path.exists() { + return Ok(()); + } + + exec_script(&script_path, block) + })?; + + Ok(()) +} + +pub fn exec_common_scripts(dir: &str, wait: bool) -> Result<()> { + let script_dir = Path::new(defs::ADB_DIR).join(dir); + if !script_dir.exists() { + info!("{} not exists, skip", script_dir.display()); + return Ok(()); + } + + let dir = fs::read_dir(&script_dir)?; + for entry in dir.flatten() { + let path = entry.path(); + + if !is_executable(&path) { + warn!("{} is not executable, skip", path.display()); + continue; + } + + exec_script(path, wait)?; + } + + Ok(()) +} + +pub fn load_system_prop() -> Result<()> { + foreach_active_module(|module| { + let system_prop = module.join("system.prop"); + if !system_prop.exists() { + return Ok(()); + } + info!("load {} system.prop", module.display()); + + // resetprop -n --file system.prop + Command::new(assets::RESETPROP_PATH) + .arg("-n") + .arg("--file") + .arg(&system_prop) + .status() + .with_context(|| format!("Failed to exec {}", system_prop.display()))?; + + Ok(()) + })?; + + Ok(()) +} + +pub fn prune_modules() -> Result<()> { + foreach_module(false, |module| { + fs::remove_file(module.join(defs::UPDATE_FILE_NAME)).ok(); + if !module.join(defs::REMOVE_FILE_NAME).exists() { + return Ok(()); + } + + info!("remove module: {}", module.display()); + + let uninstaller = module.join("uninstall.sh"); + if uninstaller.exists() { + if let Err(e) = exec_script(uninstaller, true) { + warn!("Failed to exec uninstaller: {}", e); + } + } + + if let Err(e) = fs::remove_dir_all(module) { + warn!("Failed to remove {}: {}", module.display(), e); + } + let module_path = module.display().to_string(); + let updated_path = module_path.replace(defs::MODULE_DIR, defs::MODULE_UPDATE_TMP_DIR); + + if let Err(e) = fs::remove_dir_all(&updated_path) { + warn!("Failed to remove {}: {}", updated_path, e); + } + Ok(()) + })?; + + Ok(()) +} + +fn _install_module(zip: &str) -> Result<()> { + ensure_boot_completed()?; + + // print banner + println!(include_str!("banner")); + + assets::ensure_binaries().with_context(|| "binary missing")?; + + // first check if workding dir is usable + ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?; + ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?; + + // read the module_id from zip + let mut buffer: Vec = Vec::new(); + let entry_path = PathBuf::from_str("module.prop")?; + let zip_path = PathBuf::from_str(zip)?; + let zip_path = zip_path.canonicalize()?; + zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?; + let mut module_prop = HashMap::new(); + PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into( + |k, v| { + module_prop.insert(k, v); + }, + )?; + info!("module prop: {:?}", module_prop); + + let Some(module_id) = module_prop.get("id") else { + bail!("module id not found in module.prop!"); + }; + + let modules_dir = Path::new(defs::MODULE_DIR); + let modules_update_dir = Path::new(defs::MODULE_UPDATE_TMP_DIR); + if !Path::new(modules_dir).exists() { + fs::create_dir(modules_dir).expect("Failed to create modules folder"); + let permissions = fs::Permissions::from_mode(0o700); + fs::set_permissions(modules_dir, permissions).expect("Failed to set permissions"); + } + + let module_dir = format!("{}{}", modules_dir.display(), module_id.clone()); + let _module_update_dir = format!("{}{}", modules_update_dir.display(), module_id.clone()); + info!("module dir: {}", module_dir); + if !Path::new(&module_dir.clone()).exists() { + fs::create_dir(&module_dir.clone()).expect("Failed to create module folder"); + let permissions = fs::Permissions::from_mode(0o700); + fs::set_permissions(module_dir.clone(), permissions).expect("Failed to set permissions"); + } + // unzip the image and move it to modules_update/ dir + let file = fs::File::open(zip)?; + let mut archive = zip::ZipArchive::new(file)?; + archive.extract(&_module_update_dir)?; + + // set permission and selinux context for $MOD/system + let module_system_dir = PathBuf::from(module_dir.clone()).join("system"); + if module_system_dir.exists() { + #[cfg(unix)] + fs::set_permissions(&module_system_dir, fs::Permissions::from_mode(0o755))?; + restorecon::restore_syscon(&module_system_dir)?; + } + exec_install_script(zip)?; + mark_update()?; + Ok(()) +} + +pub fn install_module(zip: &str) -> Result<()> { + let result = _install_module(zip); + result +} + +pub fn _uninstall_module(id: &str, update_dir: &str) -> Result<()> { + let dir = Path::new(update_dir); + ensure!(dir.exists(), "No module installed"); + + // iterate the modules_update dir, find the module to be removed + let dir = fs::read_dir(dir)?; + for entry in dir.flatten() { + let path = entry.path(); + let module_prop = path.join("module.prop"); + if !module_prop.exists() { + continue; + } + let content = fs::read(module_prop)?; + let mut module_id: String = String::new(); + PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8).read_into( + |k, v| { + if k.eq("id") { + module_id = v; + } + }, + )?; + if module_id.eq(id) { + let remove_file = path.join(defs::REMOVE_FILE_NAME); + fs::File::create(remove_file).with_context(|| "Failed to create remove file.")?; + break; + } + } + + // santity check + let target_module_path = format!("{update_dir}/{id}"); + let target_module = Path::new(&target_module_path); + if target_module.exists() { + let remove_file = target_module.join(defs::REMOVE_FILE_NAME); + if !remove_file.exists() { + fs::File::create(remove_file).with_context(|| "Failed to create remove file.")?; + } + } + + let _ = mark_module_state(id, defs::REMOVE_FILE_NAME, true); + Ok(()) +} +pub fn uninstall_module(id: &str) -> Result<()> { + _uninstall_module(id, defs::MODULE_DIR)?; + mark_update()?; + Ok(()) +} + +pub fn run_action(id: &str) -> Result<()> { + let action_script_path = format!("/data/adb/modules/{}/action.sh", id); + let _ = exec_script(&action_script_path, true); + Ok(()) +} + +fn _change_module_state(module_dir: &str, mid: &str, enable: bool) -> Result<()> { + let src_module_path = format!("{module_dir}/{mid}"); + let src_module = Path::new(&src_module_path); + ensure!(src_module.exists(), "module: {} not found!", mid); + + let disable_path = src_module.join(defs::DISABLE_FILE_NAME); + if enable { + if disable_path.exists() { + fs::remove_file(&disable_path).with_context(|| { + format!("Failed to remove disable file: {}", &disable_path.display()) + })?; + } + } else { + ensure_file_exists(disable_path)?; + } + + let _ = mark_module_state(mid, defs::DISABLE_FILE_NAME, !enable); + + Ok(()) +} + +pub fn _enable_module(id: &str, update_dir: &Path) -> Result<()> { + if let Some(module_dir_str) = update_dir.to_str() { + _change_module_state(module_dir_str, id, true) + } else { + info!("Enable module failed: Invalid path"); + Err(anyhow::anyhow!("Invalid module directory")) + } +} + +pub fn enable_module(id: &str) -> Result<()> { + let update_dir = Path::new(defs::MODULE_DIR); + _enable_module(id, update_dir)?; + Ok(()) +} + +pub fn _disable_module(id: &str, update_dir: &Path) -> Result<()> { + if let Some(module_dir_str) = update_dir.to_str() { + _change_module_state(module_dir_str, id, false) + } else { + info!("Disable module failed: Invalid path"); + Err(anyhow::anyhow!("Invalid module directory")) + } +} + +pub fn disable_module(id: &str) -> Result<()> { + let module_dir = Path::new(defs::MODULE_DIR); + _disable_module(id, module_dir)?; + + Ok(()) +} + +pub fn _disable_all_modules(dir: &str) -> Result<()> { + let dir = fs::read_dir(dir)?; + for entry in dir.flatten() { + let path = entry.path(); + let disable_flag = path.join(defs::DISABLE_FILE_NAME); + if let Err(e) = ensure_file_exists(disable_flag) { + warn!("Failed to disable module: {}: {}", path.display(), e); + } + } + Ok(()) +} + +pub fn disable_all_modules() -> Result<()> { + // Skip disabling modules since boot completed + if getprop("sys.boot_completed").as_deref() == Some("1") { + info!("System boot completed, no need to disable all modules"); + return Ok(()); + } + mark_update()?; + _disable_all_modules(defs::MODULE_DIR)?; + Ok(()) +} + +fn _list_modules(path: &str) -> Vec> { + // first check enabled modules + let dir = fs::read_dir(path); + let Ok(dir) = dir else { + return Vec::new(); + }; + + let mut modules: Vec> = Vec::new(); + + for entry in dir.flatten() { + let path = entry.path(); + info!("path: {}", path.display()); + let module_prop = path.join("module.prop"); + if !module_prop.exists() { + continue; + } + let content = fs::read(&module_prop); + let Ok(content) = content else { + warn!("Failed to read file: {}", module_prop.display()); + continue; + }; + let mut module_prop_map: HashMap = HashMap::new(); + let encoding = encoding_rs::UTF_8; + let result = + PropertiesIter::new_with_encoding(Cursor::new(content), encoding).read_into(|k, v| { + module_prop_map.insert(k, v); + }); + + if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() { + match entry.file_name().to_str() { + Some(id) => { + info!("Use dir name as module id: {}", id); + module_prop_map.insert("id".to_owned(), id.to_owned()); + } + _ => { + info!("Failed to get module id: {:?}", module_prop); + continue; + } + } + } + + // Add enabled, update, remove flags + let enabled = !path.join(defs::DISABLE_FILE_NAME).exists(); + let update = path.join(defs::UPDATE_FILE_NAME).exists(); + let remove = path.join(defs::REMOVE_FILE_NAME).exists(); + let web = path.join(defs::MODULE_WEB_DIR).exists(); + let action = path.join(defs::MODULE_ACTION_SH).exists(); + + module_prop_map.insert("enabled".to_owned(), enabled.to_string()); + module_prop_map.insert("update".to_owned(), update.to_string()); + module_prop_map.insert("remove".to_owned(), remove.to_string()); + module_prop_map.insert("web".to_owned(), web.to_string()); + module_prop_map.insert("action".to_owned(), action.to_string()); + + if result.is_err() { + warn!("Failed to parse module.prop: {}", module_prop.display()); + continue; + } + modules.push(module_prop_map); + } + + modules +} + +pub fn list_modules() -> Result<()> { + let modules = _list_modules(defs::MODULE_DIR); + println!("{}", serde_json::to_string_pretty(&modules)?); + Ok(()) +} diff --git a/apd/src/mount.rs b/apd/src/mount.rs new file mode 100644 index 0000000..d7d49a5 --- /dev/null +++ b/apd/src/mount.rs @@ -0,0 +1,371 @@ +#[cfg(any(target_os = "linux", target_os = "android"))] +use anyhow::Context; +use anyhow::{Ok, Result, anyhow, bail}; +#[cfg(any(target_os = "linux", target_os = "android"))] +#[allow(unused_imports)] +use retry::delay::NoDelay; +#[cfg(any(target_os = "linux", target_os = "android"))] +//use sys_mount::{unmount, FilesystemType, Mount, MountFlags, Unmount, UnmountFlags}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use rustix::{fd::AsFd, fs::CWD, mount::*}; +use std::fs::create_dir; +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::os::unix::fs::PermissionsExt; + +use crate::defs::AP_OVERLAY_SOURCE; +use crate::defs::PTS_NAME; +use log::{info, warn}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use procfs::process::Process; +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +pub struct AutoMountExt4 { + target: String, + auto_umount: bool, +} + +impl AutoMountExt4 { + #[cfg(any(target_os = "linux", target_os = "android"))] + + pub fn try_new(source: &str, target: &str, auto_umount: bool) -> Result { + let path = Path::new(source); + if !path.exists() { + println!("Source path does not exist"); + } else { + let metadata = fs::metadata(path)?; + let permissions = metadata.permissions(); + let mode = permissions.mode(); + + if permissions.readonly() { + #[cfg(any(target_os = "linux", target_os = "android"))] + println!("File permissions: {:o} (octal)", mode & 0o777); + } + } + + mount_ext4(source, target)?; + Ok(Self { + target: target.to_string(), + auto_umount, + }) + } + #[cfg(not(any(target_os = "linux", target_os = "android")))] + pub fn try_new(_src: &str, _mnt: &str, _auto_umount: bool) -> Result { + unimplemented!() + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + pub fn umount(&self) -> Result<()> { + unmount(self.target.as_str(), UnmountFlags::DETACH)?; + Ok(()) + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +impl Drop for AutoMountExt4 { + fn drop(&mut self) { + info!( + "AutoMountExt4 drop: {}, auto_umount: {}", + self.target, self.auto_umount + ); + if self.auto_umount { + let _ = self.umount(); + } + } +} + +#[allow(dead_code)] +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_image(src: &str, target: &str, _autodrop: bool) -> Result<()> { + mount_ext4(src, target)?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_ext4(source: impl AsRef, target: impl AsRef) -> Result<()> { + let new_loopback = loopdev::LoopControl::open()?.next_free()?; + new_loopback.with().attach(source)?; + let lo = new_loopback.path().ok_or(anyhow!("no loop"))?; + match fsopen("ext4", FsOpenFlags::FSOPEN_CLOEXEC) { + Result::Ok(fs) => { + let fs = fs.as_fd(); + fsconfig_set_string(fs, "source", lo)?; + fsconfig_create(fs)?; + let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount( + mount.as_fd(), + "", + CWD, + target.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + )?; + } + _ => { + mount(lo, target.as_ref(), "ext4", MountFlags::empty(), "")?; + } + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn umount_dir(src: impl AsRef) -> Result<()> { + unmount(src.as_ref(), UnmountFlags::empty()) + .with_context(|| format!("Failed to umount {}", src.as_ref().display()))?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlayfs( + lower_dirs: &[String], + lowest: &str, + upperdir: Option, + workdir: Option, + dest: impl AsRef, +) -> Result<()> { + let lowerdir_config = lower_dirs + .iter() + .map(|s| s.as_ref()) + .chain(std::iter::once(lowest)) + .collect::>() + .join(":"); + info!( + "mount overlayfs on {:?}, lowerdir={}, upperdir={:?}, workdir={:?}", + dest.as_ref(), + lowerdir_config, + upperdir, + workdir + ); + + let upperdir = upperdir + .filter(|up| up.exists()) + .map(|e| e.display().to_string()); + let workdir = workdir + .filter(|wd| wd.exists()) + .map(|e| e.display().to_string()); + + let result = (|| { + let fs = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?; + let fs = fs.as_fd(); + fsconfig_set_string(fs, "lowerdir", &lowerdir_config)?; + if let (Some(upperdir), Some(workdir)) = (&upperdir, &workdir) { + fsconfig_set_string(fs, "upperdir", upperdir)?; + fsconfig_set_string(fs, "workdir", workdir)?; + } + fsconfig_set_string(fs, "source", AP_OVERLAY_SOURCE)?; + fsconfig_create(fs)?; + let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount( + mount.as_fd(), + "", + CWD, + dest.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + ) + })(); + + if let Err(e) = result { + warn!("fsopen mount failed: {:#}, fallback to mount", e); + let mut data = format!("lowerdir={lowerdir_config}"); + if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) { + data = format!("{data},upperdir={upperdir},workdir={workdir}"); + } + mount( + AP_OVERLAY_SOURCE, + dest.as_ref(), + "overlay", + MountFlags::empty(), + data, + )?; + } + Ok(()) +} +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_devpts(dest: impl AsRef) -> Result<()> { + create_dir(dest.as_ref())?; + mount( + AP_OVERLAY_SOURCE, + dest.as_ref(), + "devpts", + MountFlags::empty(), + "newinstance", + )?; + mount_change(dest.as_ref(), MountPropagationFlags::PRIVATE).context("make devpts private")?; + Ok(()) +} +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_devpts(_dest: impl AsRef) -> Result<()> { + unimplemented!() +} +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_tmpfs(dest: impl AsRef) -> Result<()> { + info!("mount tmpfs on {}", dest.as_ref().display()); + match fsopen("tmpfs", FsOpenFlags::FSOPEN_CLOEXEC) { + Result::Ok(fs) => { + let fs = fs.as_fd(); + fsconfig_set_string(fs, "source", AP_OVERLAY_SOURCE)?; + fsconfig_create(fs)?; + let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount( + mount.as_fd(), + "", + CWD, + dest.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + )?; + } + _ => { + mount( + AP_OVERLAY_SOURCE, + dest.as_ref(), + "tmpfs", + MountFlags::empty(), + "", + )?; + } + } + mount_change(dest.as_ref(), MountPropagationFlags::PRIVATE).context("make tmpfs private")?; + let pts_dir = format!("{}/{PTS_NAME}", dest.as_ref().display()); + if let Err(e) = mount_devpts(pts_dir) { + warn!("do devpts mount failed: {}", e); + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn bind_mount(from: impl AsRef, to: impl AsRef) -> Result<()> { + info!( + "bind mount {} -> {}", + from.as_ref().display(), + to.as_ref().display() + ); + match open_tree( + CWD, + from.as_ref(), + OpenTreeFlags::OPEN_TREE_CLOEXEC + | OpenTreeFlags::OPEN_TREE_CLONE + | OpenTreeFlags::AT_RECURSIVE, + ) { + Result::Ok(tree) => { + move_mount( + tree.as_fd(), + "", + CWD, + to.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + )?; + } + _ => { + mount( + from.as_ref(), + to.as_ref(), + "", + MountFlags::BIND | MountFlags::REC, + "", + )?; + } + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn mount_overlay_child( + mount_point: &str, + relative: &String, + module_roots: &Vec, + stock_root: &String, +) -> Result<()> { + if !module_roots + .iter() + .any(|lower| Path::new(&format!("{lower}{relative}")).exists()) + { + return bind_mount(stock_root, mount_point); + } + if !Path::new(&stock_root).is_dir() { + return Ok(()); + } + let mut lower_dirs: Vec = vec![]; + for lower in module_roots { + let lower_dir = format!("{lower}{relative}"); + let path = Path::new(&lower_dir); + if path.is_dir() { + lower_dirs.push(lower_dir); + } else if path.exists() { + // stock root has been blocked by this file + return Ok(()); + } + } + if lower_dirs.is_empty() { + return Ok(()); + } + // merge modules and stock + if let Err(e) = mount_overlayfs(&lower_dirs, stock_root, None, None, mount_point) { + warn!("failed: {:#}, fallback to bind mount", e); + bind_mount(stock_root, mount_point)?; + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlay( + root: &String, + module_roots: &Vec, + workdir: Option, + upperdir: Option, +) -> Result<()> { + info!("mount overlay for {}", root); + std::env::set_current_dir(root).with_context(|| format!("failed to chdir to {root}"))?; + let stock_root = "."; + + // collect child mounts before mounting the root + let mounts = Process::myself()? + .mountinfo() + .with_context(|| "get mountinfo")?; + let mut mount_seq = mounts + .0 + .iter() + .filter(|m| { + m.mount_point.starts_with(root) && !Path::new(&root).starts_with(&m.mount_point) + }) + .map(|m| m.mount_point.to_str()) + .collect::>(); + mount_seq.sort(); + mount_seq.dedup(); + + mount_overlayfs(module_roots, root, upperdir, workdir, root) + .with_context(|| "mount overlayfs for root failed")?; + for mount_point in mount_seq.iter() { + let Some(mount_point) = mount_point else { + continue; + }; + let relative = mount_point.replacen(root, "", 1); + let stock_root: String = format!("{stock_root}{relative}"); + if !Path::new(&stock_root).exists() { + continue; + } + if let Err(e) = mount_overlay_child(mount_point, &relative, module_roots, &stock_root) { + warn!( + "failed to mount overlay for child {}: {:#}, revert", + mount_point, e + ); + umount_dir(root).with_context(|| format!("failed to revert {root}"))?; + bail!(e); + } + } + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_ext4(_src: &str, _target: &str, _autodrop: bool) -> Result<()> { + unimplemented!() +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn umount_dir(_src: &str) -> Result<()> { + unimplemented!() +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_overlay(_dest: &String, _lower_dirs: &Vec) -> Result<()> { + unimplemented!() +} diff --git a/apd/src/package.rs b/apd/src/package.rs new file mode 100644 index 0000000..bb1af34 --- /dev/null +++ b/apd/src/package.rs @@ -0,0 +1,178 @@ +use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::Path; +use std::thread; +use std::time::Duration; + +#[derive(Deserialize, Serialize, Clone)] +pub struct PackageConfig { + pub pkg: String, + pub exclude: i32, + pub allow: i32, + pub uid: i32, + pub to_uid: i32, + pub sctx: String, +} + +pub fn read_ap_package_config() -> Vec { + let max_retry = 5; + for _ in 0..max_retry { + let file = match File::open("/data/adb/ap/package_config") { + Ok(file) => file, + Err(e) => { + warn!("Error opening file: {}", e); + thread::sleep(Duration::from_secs(1)); + continue; + } + }; + + let mut reader = csv::Reader::from_reader(file); + let mut package_configs = Vec::new(); + let mut success = true; + + for record in reader.deserialize() { + match record { + Ok(config) => package_configs.push(config), + Err(e) => { + warn!("Error deserializing record: {}", e); + success = false; + break; + } + } + } + + if success { + return package_configs; + } + thread::sleep(Duration::from_secs(1)); + } + Vec::new() +} + +pub fn write_ap_package_config(package_configs: &[PackageConfig]) -> io::Result<()> { + let max_retry = 5; + for _ in 0..max_retry { + let temp_path = "/data/adb/ap/package_config.tmp"; + let file = match File::create(temp_path) { + Ok(file) => file, + Err(e) => { + warn!("Error creating temp file: {}", e); + thread::sleep(Duration::from_secs(1)); + continue; + } + }; + + let mut writer = csv::Writer::from_writer(file); + let mut success = true; + + for config in package_configs { + if let Err(e) = writer.serialize(config) { + warn!("Error serializing record: {}", e); + success = false; + break; + } + } + + if !success { + thread::sleep(Duration::from_secs(1)); + continue; + } + + if let Err(e) = writer.flush() { + warn!("Error flushing writer: {}", e); + thread::sleep(Duration::from_secs(1)); + continue; + } + + if let Err(e) = std::fs::rename(temp_path, "/data/adb/ap/package_config") { + warn!("Error renaming temp file: {}", e); + thread::sleep(Duration::from_secs(1)); + continue; + } + return Ok(()); + } + Err(io::Error::new( + io::ErrorKind::Other, + "Failed after max retries", + )) +} + +fn read_lines

(filename: P) -> io::Result>> +where + P: AsRef, +{ + File::open(filename).map(|file| io::BufReader::new(file).lines()) +} + +pub fn synchronize_package_uid() -> io::Result<()> { + info!("[synchronize_package_uid] Start synchronizing root list with system packages..."); + + let max_retry = 5; + for _ in 0..max_retry { + match read_lines("/data/system/packages.list") { + Ok(lines) => { + let lines: Vec<_> = lines.filter_map(|line| line.ok()).collect(); + + let mut package_configs = read_ap_package_config(); + + let system_packages: Vec = lines + .iter() + .filter_map(|line| line.split_whitespace().next()) + .map(|pkg| pkg.to_string()) + .collect(); + + let original_len = package_configs.len(); + package_configs.retain(|config| system_packages.contains(&config.pkg)); + let removed_count = original_len - package_configs.len(); + + if removed_count > 0 { + info!( + "Removed {} uninstalled package configurations", + removed_count + ); + } + + let mut updated = false; + + for line in &lines { + let words: Vec<&str> = line.split_whitespace().collect(); + if words.len() >= 2 { + let pkg_name = words[0]; + if let Ok(uid) = words[1].parse::() { + if let Some(config) = package_configs + .iter_mut() + .find(|config| config.pkg == pkg_name) + { + if config.uid != uid { + info!( + "Updating uid for package {}: {} -> {}", + pkg_name, config.uid, uid + ); + config.uid = uid; + updated = true; + } + } + } else { + warn!("Error parsing uid: {}", words[1]); + } + } + } + + if updated || removed_count > 0 { + write_ap_package_config(&package_configs)?; + } + return Ok(()); + } + Err(e) => { + warn!("Error reading packages.list: {}", e); + thread::sleep(Duration::from_secs(1)); + } + } + } + Err(io::Error::new( + io::ErrorKind::Other, + "Failed after max retries", + )) +} diff --git a/apd/src/pty.rs b/apd/src/pty.rs new file mode 100644 index 0000000..5c6991d --- /dev/null +++ b/apd/src/pty.rs @@ -0,0 +1,185 @@ +use std::ffi::c_int; +use std::fs::File; +use std::io::{Read, Write, stderr, stdin, stdout}; +use std::mem::MaybeUninit; +use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd}; +use std::process::exit; +use std::ptr::null_mut; +use std::thread; + +use crate::defs::PTS_NAME; +use crate::utils::get_tmp_path; +use anyhow::{Ok, Result, bail}; +use libc::{ + __errno, EINTR, SIG_BLOCK, SIG_UNBLOCK, SIGWINCH, TIOCGWINSZ, TIOCSWINSZ, fork, + pthread_sigmask, sigaddset, sigemptyset, sigset_t, sigwait, waitpid, winsize, +}; +use rustix::fs::{Mode, OFlags, open}; +use rustix::io::dup; +use rustix::ioctl::{Getter, ReadOpcode, ioctl}; +use rustix::process::setsid; +use rustix::pty::{grantpt, unlockpt}; +use rustix::stdio::{dup2_stderr, dup2_stdin, dup2_stdout}; +use rustix::termios::{OptionalActions, Termios, isatty, tcgetattr, tcsetattr}; +use std::sync::Mutex; + +// https://github.com/topjohnwu/Magisk/blob/5627053b7481618adfdf8fa3569b48275589915b/native/src/core/su/pts.cpp + +fn get_pty_num(fd: F) -> Result { + Ok(unsafe { + let tiocgptn = Getter::, u32>::new(); + ioctl(fd, tiocgptn)? + }) +} + +static OLD_STDIN: Mutex> = Mutex::new(None); + +fn watch_sigwinch_async(slave: RawFd) { + let mut winch = MaybeUninit::::uninit(); + unsafe { + sigemptyset(winch.as_mut_ptr()); + sigaddset(winch.as_mut_ptr(), SIGWINCH); + pthread_sigmask(SIG_BLOCK, winch.as_mut_ptr(), null_mut()); + } + + thread::spawn(move || unsafe { + let mut winch = MaybeUninit::::uninit(); + sigemptyset(winch.as_mut_ptr()); + sigaddset(winch.as_mut_ptr(), SIGWINCH); + pthread_sigmask(SIG_UNBLOCK, winch.as_mut_ptr(), null_mut()); + let mut sig: c_int = 0; + loop { + let mut w = MaybeUninit::::uninit(); + if libc::ioctl(1, TIOCGWINSZ, w.as_mut_ptr()) < 0 { + continue; + } + libc::ioctl(slave, TIOCSWINSZ, w.as_mut_ptr()); + if sigwait(winch.as_mut_ptr(), &mut sig) != 0 { + break; + } + } + }); +} + +fn set_stdin_raw() -> rustix::io::Result<()> { + let mut termios = tcgetattr(stdin())?; + + let mut guard = OLD_STDIN.lock().unwrap(); + *guard = Some(termios.clone()); + drop(guard); + + termios.make_raw(); + tcsetattr(stdin(), OptionalActions::Flush, &termios) +} + +fn restore_stdin() -> Result<()> { + let mut guard = OLD_STDIN.lock().unwrap(); + + if let Some(original_termios) = guard.take() { + tcsetattr(stdin(), OptionalActions::Flush, &original_termios)?; + } + + Ok(()) +} + +fn pump(mut from: R, mut to: W) { + let mut buf = [0u8; 4096]; + loop { + match from.read(&mut buf) { + Result::Ok(len) => { + if len == 0 { + return; + } + if to.write_all(&buf[0..len]).is_err() { + return; + } + if to.flush().is_err() { + return; + } + } + Err(_) => { + return; + } + } + } +} + +fn pump_stdin_async(mut ptmx: File) { + let _ = set_stdin_raw(); + + thread::spawn(move || { + let mut stdin = stdin(); + pump(&mut stdin, &mut ptmx); + }); +} + +fn pump_stdout_blocking(mut ptmx: File) { + let mut stdout = stdout(); + pump(&mut ptmx, &mut stdout); + + let _ = restore_stdin(); +} + +fn create_transfer(ptmx: OwnedFd) -> Result<()> { + let pid = unsafe { fork() }; + match pid { + d if d < 0 => bail!("fork"), + 0 => return Ok(()), + _ => {} + } + + let ptmx_r = ptmx; + let ptmx_w = dup(&ptmx_r)?; + + let ptmx_r = File::from(ptmx_r); + let ptmx_w = File::from(ptmx_w); + + watch_sigwinch_async(ptmx_w.as_raw_fd()); + pump_stdin_async(ptmx_r); + pump_stdout_blocking(ptmx_w); + + let mut status: c_int = -1; + + unsafe { + loop { + if waitpid(pid, &mut status, 0) == -1 && *__errno() != EINTR { + continue; + } + break; + } + } + + exit(status) +} + +pub fn prepare_pty() -> Result<()> { + let tty_in = isatty(stdin()); + let tty_out = isatty(stdout()); + let tty_err = isatty(stderr()); + if !tty_in && !tty_out && !tty_err { + return Ok(()); + } + + let mut pts_path = format!("{}/{}", get_tmp_path(), PTS_NAME); + if !std::path::Path::new(&pts_path).exists() { + pts_path = "/dev/pts".to_string(); + } + let ptmx_path = format!("{}/ptmx", pts_path); + let ptmx_fd = open(ptmx_path, OFlags::RDWR, Mode::empty())?; + grantpt(&ptmx_fd)?; + unlockpt(&ptmx_fd)?; + let pty_num = get_pty_num(&ptmx_fd)?; + create_transfer(ptmx_fd)?; + setsid()?; + let pty_fd = open(format!("{pts_path}/{pty_num}"), OFlags::RDWR, Mode::empty())?; + if tty_in { + dup2_stdin(&pty_fd)?; + } + if tty_out { + dup2_stdout(&pty_fd)?; + } + if tty_err { + dup2_stderr(&pty_fd)?; + } + Ok(()) +} diff --git a/apd/src/restorecon.rs b/apd/src/restorecon.rs new file mode 100644 index 0000000..bf017b9 --- /dev/null +++ b/apd/src/restorecon.rs @@ -0,0 +1,81 @@ +use crate::defs; +use anyhow::Result; +use jwalk::{Parallelism::Serial, WalkDir}; +use std::path::Path; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use anyhow::{Context, Ok}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use extattr::{Flags as XattrFlags, lsetxattr}; + +pub const SYSTEM_CON: &str = "u:object_r:system_file:s0"; +pub const ADB_CON: &str = "u:object_r:adb_data_file:s0"; +pub const UNLABEL_CON: &str = "u:object_r:unlabeled:s0"; + +const SELINUX_XATTR: &str = "security.selinux"; + +pub fn lsetfilecon>(path: P, con: &str) -> Result<()> { + #[cfg(any(target_os = "linux", target_os = "android"))] + lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| { + format!( + "Failed to change SELinux context for {}", + path.as_ref().display() + ) + })?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn lgetfilecon>(path: P) -> Result { + let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| { + format!( + "Failed to get SELinux context for {}", + path.as_ref().display() + ) + })?; + let con = String::from_utf8_lossy(&con); + Ok(con.to_string()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn setsyscon>(path: P) -> Result<()> { + lsetfilecon(path, SYSTEM_CON) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn setsyscon>(path: P) -> Result<()> { + unimplemented!() +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn lgetfilecon>(path: P) -> Result { + unimplemented!() +} + +pub fn restore_syscon>(dir: P) -> Result<()> { + for dir_entry in WalkDir::new(dir).parallelism(Serial) { + if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) { + setsyscon(&path)?; + } + } + Ok(()) +} + +fn restore_syscon_if_unlabeled>(dir: P) -> Result<()> { + for dir_entry in WalkDir::new(dir).parallelism(Serial) { + if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) { + if let Result::Ok(con) = lgetfilecon(&path) { + if con == UNLABEL_CON || con.is_empty() { + lsetfilecon(&path, SYSTEM_CON)?; + } + } + } + } + Ok(()) +} + +pub fn restorecon() -> Result<()> { + lsetfilecon(defs::DAEMON_PATH, ADB_CON)?; + restore_syscon_if_unlabeled(defs::MODULE_DIR)?; + Ok(()) +} diff --git a/apd/src/sepolicy.rs b/apd/src/sepolicy.rs new file mode 100644 index 0000000..7dc93aa --- /dev/null +++ b/apd/src/sepolicy.rs @@ -0,0 +1,703 @@ +use anyhow::{Result, bail}; +use derive_new::new; +use nom::{ + AsChar, IResult, Parser, + branch::alt, + bytes::complete::{tag, take_while, take_while_m_n, take_while1}, + character::complete::{space0, space1}, + combinator::map, +}; +use std::{ffi, path::Path, vec}; + +type SeObject<'a> = Vec<&'a str>; + +fn is_sepolicy_char(c: char) -> bool { + c.is_alphanum() || c == '_' || c == '-' +} + +fn parse_single_word(input: &str) -> IResult<&str, &str> { + take_while1(is_sepolicy_char).parse(input) +} + +fn parse_bracket_objs(input: &str) -> IResult<&str, SeObject> { + let (input, (_, words, _)) = ( + tag("{"), + take_while_m_n(1, 100, |c: char| is_sepolicy_char(c) || c.is_whitespace()), + tag("}"), + ) + .parse(input)?; + Ok((input, words.split_whitespace().collect())) +} + +fn parse_single_obj(input: &str) -> IResult<&str, SeObject> { + let (input, word) = take_while1(is_sepolicy_char).parse(input)?; + Ok((input, vec![word])) +} + +fn parse_star(input: &str) -> IResult<&str, SeObject> { + let (input, _) = tag("*").parse(input)?; + Ok((input, vec!["*"])) +} + +// 1. a single sepolicy word +// 2. { obj1 obj2 obj3 ...} +// 3. * +fn parse_seobj(input: &str) -> IResult<&str, SeObject> { + let (input, strs) = alt((parse_single_obj, parse_bracket_objs, parse_star)).parse(input)?; + Ok((input, strs)) +} + +fn parse_seobj_no_star(input: &str) -> IResult<&str, SeObject> { + let (input, strs) = alt((parse_single_obj, parse_bracket_objs)).parse(input)?; + Ok((input, strs)) +} + +trait SeObjectParser<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> + where + Self: Sized; +} + +#[derive(Debug, PartialEq, Eq, new)] +struct NormalPerm<'a> { + op: &'a str, + source: SeObject<'a>, + target: SeObject<'a>, + class: SeObject<'a>, + perm: SeObject<'a>, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct XPerm<'a> { + op: &'a str, + source: SeObject<'a>, + target: SeObject<'a>, + class: SeObject<'a>, + operation: &'a str, + perm_set: &'a str, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct TypeState<'a> { + op: &'a str, + stype: SeObject<'a>, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct TypeAttr<'a> { + stype: SeObject<'a>, + sattr: SeObject<'a>, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct Type<'a> { + name: &'a str, + attrs: SeObject<'a>, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct Attr<'a> { + name: &'a str, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct TypeTransition<'a> { + source: &'a str, + target: &'a str, + class: &'a str, + default_type: &'a str, + object_name: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct TypeChange<'a> { + op: &'a str, + source: &'a str, + target: &'a str, + class: &'a str, + default_type: &'a str, +} + +#[derive(Debug, PartialEq, Eq, new)] +struct GenFsCon<'a> { + fs_name: &'a str, + partial_path: &'a str, + fs_context: &'a str, +} + +#[derive(Debug)] +enum PolicyStatement<'a> { + // "allow *source_type *target_type *class *perm_set" + // "deny *source_type *target_type *class *perm_set" + // "auditallow *source_type *target_type *class *perm_set" + // "dontaudit *source_type *target_type *class *perm_set" + NormalPerm(NormalPerm<'a>), + + // "allowxperm *source_type *target_type *class operation xperm_set" + // "auditallowxperm *source_type *target_type *class operation xperm_set" + // "dontauditxperm *source_type *target_type *class operation xperm_set" + XPerm(XPerm<'a>), + + // "permissive ^type" + // "enforce ^type" + TypeState(TypeState<'a>), + + // "type type_name ^(attribute)" + Type(Type<'a>), + + // "typeattribute ^type ^attribute" + TypeAttr(TypeAttr<'a>), + + // "attribute ^attribute" + Attr(Attr<'a>), + + // "type_transition source_type target_type class default_type (object_name)" + TypeTransition(TypeTransition<'a>), + + // "type_change source_type target_type class default_type" + // "type_member source_type target_type class default_type" + TypeChange(TypeChange<'a>), + + // "genfscon fs_name partial_path fs_context" + GenFsCon(GenFsCon<'a>), +} + +impl<'a> SeObjectParser<'a> for NormalPerm<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, op) = alt(( + tag("allow"), + tag("deny"), + tag("auditallow"), + tag("dontaudit"), + )) + .parse(input)?; + + let (input, _) = space0(input)?; + let (input, source) = parse_seobj(input)?; + let (input, _) = space0(input)?; + let (input, target) = parse_seobj(input)?; + let (input, _) = space0(input)?; + let (input, class) = parse_seobj(input)?; + let (input, _) = space0(input)?; + let (input, perm) = parse_seobj(input)?; + Ok((input, NormalPerm::new(op, source, target, class, perm))) + } +} + +impl<'a> SeObjectParser<'a> for XPerm<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, op) = alt(( + tag("allowxperm"), + tag("auditallowxperm"), + tag("dontauditxperm"), + )) + .parse(input)?; + + let (input, _) = space0(input)?; + let (input, source) = parse_seobj(input)?; + let (input, _) = space0(input)?; + let (input, target) = parse_seobj(input)?; + let (input, _) = space0(input)?; + let (input, class) = parse_seobj(input)?; + let (input, _) = space0(input)?; + let (input, operation) = parse_single_word(input)?; + let (input, _) = space0(input)?; + let (input, perm_set) = parse_single_word(input)?; + + Ok(( + input, + XPerm::new(op, source, target, class, operation, perm_set), + )) + } +} + +impl<'a> SeObjectParser<'a> for TypeState<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, op) = alt((tag("permissive"), tag("enforce"))).parse(input)?; + + let (input, _) = space1(input)?; + let (input, stype) = parse_seobj_no_star(input)?; + + Ok((input, TypeState::new(op, stype))) + } +} + +impl<'a> SeObjectParser<'a> for Type<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, _) = tag("type")(input)?; + let (input, _) = space1(input)?; + let (input, name) = parse_single_word(input)?; + + if input.is_empty() { + return Ok((input, Type::new(name, vec!["domain"]))); // default to domain + } + + let (input, _) = space1(input)?; + let (input, attrs) = parse_seobj_no_star(input)?; + + Ok((input, Type::new(name, attrs))) + } +} + +impl<'a> SeObjectParser<'a> for TypeAttr<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, _) = alt((tag("typeattribute"), tag("attradd"))).parse(input)?; + let (input, _) = space1(input)?; + let (input, stype) = parse_seobj_no_star(input)?; + let (input, _) = space1(input)?; + let (input, attr) = parse_seobj_no_star(input)?; + + Ok((input, TypeAttr::new(stype, attr))) + } +} + +impl<'a> SeObjectParser<'a> for Attr<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, _) = tag("attribute")(input)?; + let (input, _) = space1(input)?; + let (input, attr) = parse_single_word(input)?; + + Ok((input, Attr::new(attr))) + } +} + +impl<'a> SeObjectParser<'a> for TypeTransition<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, _) = alt((tag("type_transition"), tag("name_transition"))).parse(input)?; + let (input, _) = space1(input)?; + let (input, source) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, target) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, class) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, default) = parse_single_word(input)?; + + if input.is_empty() { + return Ok(( + input, + TypeTransition::new(source, target, class, default, None), + )); + } + + let (input, _) = space1(input)?; + let (input, object) = parse_single_word(input)?; + + Ok(( + input, + TypeTransition::new(source, target, class, default, Some(object)), + )) + } +} + +impl<'a> SeObjectParser<'a> for TypeChange<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, op) = alt((tag("type_change"), tag("type_member"))).parse(input)?; + let (input, _) = space1(input)?; + let (input, source) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, target) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, class) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, default) = parse_single_word(input)?; + + Ok((input, TypeChange::new(op, source, target, class, default))) + } +} + +impl<'a> SeObjectParser<'a> for GenFsCon<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> + where + Self: Sized, + { + let (input, _) = tag("genfscon")(input)?; + let (input, _) = space1(input)?; + let (input, fs) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, path) = parse_single_word(input)?; + let (input, _) = space1(input)?; + let (input, context) = parse_single_word(input)?; + Ok((input, GenFsCon::new(fs, path, context))) + } +} + +impl<'a> PolicyStatement<'a> { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + let (input, _) = space0(input)?; + let (input, statement) = alt(( + map(NormalPerm::parse, PolicyStatement::NormalPerm), + map(XPerm::parse, PolicyStatement::XPerm), + map(TypeState::parse, PolicyStatement::TypeState), + map(Type::parse, PolicyStatement::Type), + map(TypeAttr::parse, PolicyStatement::TypeAttr), + map(Attr::parse, PolicyStatement::Attr), + map(TypeTransition::parse, PolicyStatement::TypeTransition), + map(TypeChange::parse, PolicyStatement::TypeChange), + map(GenFsCon::parse, PolicyStatement::GenFsCon), + )) + .parse(input)?; + let (input, _) = space0(input)?; + let (input, _) = take_while(|c| c == ';')(input)?; + let (input, _) = space0(input)?; + Ok((input, statement)) + } +} + +fn parse_sepolicy<'a, 'b>(input: &'b str, strict: bool) -> Result>> +where + 'b: 'a, +{ + let mut statements = vec![]; + + for line in input.split(['\n', ';']) { + let trimmed_line = line.trim(); + if trimmed_line.is_empty() || trimmed_line.starts_with('#') { + continue; + } + if let Ok((_, statement)) = PolicyStatement::parse(trimmed_line) { + statements.push(statement); + } else if strict { + bail!("Failed to parse policy statement: {}", line) + } + } + Ok(statements) +} + +const SEPOLICY_MAX_LEN: usize = 128; + +const CMD_NORMAL_PERM: u32 = 1; +const CMD_XPERM: u32 = 2; +const CMD_TYPE_STATE: u32 = 3; +const CMD_TYPE: u32 = 4; +const CMD_TYPE_ATTR: u32 = 5; +const CMD_ATTR: u32 = 6; +const CMD_TYPE_TRANSITION: u32 = 7; +const CMD_TYPE_CHANGE: u32 = 8; +const CMD_GENFSCON: u32 = 9; + +#[derive(Debug, Default)] +enum PolicyObject { + All, // for "*", stand for all objects, and is NULL in ffi + One([u8; SEPOLICY_MAX_LEN]), + #[default] + None, +} + +impl TryFrom<&str> for PolicyObject { + type Error = anyhow::Error; + fn try_from(s: &str) -> Result { + anyhow::ensure!(s.len() <= SEPOLICY_MAX_LEN, "policy object too long"); + if s == "*" { + return Ok(PolicyObject::All); + } + let mut buf = [0u8; SEPOLICY_MAX_LEN]; + buf[..s.len()].copy_from_slice(s.as_bytes()); + Ok(PolicyObject::One(buf)) + } +} + +/// atomic statement, such as: allow domain1 domain2:file1 read; +/// normal statement would be expanded to atomic statement, for example: +/// allow domain1 domain2:file1 { read write }; would be expanded to two atomic statement +/// allow domain1 domain2:file1 read;allow domain1 domain2:file1 write; +#[allow(clippy::too_many_arguments)] +#[derive(Debug, new)] +struct AtomicStatement { + cmd: u32, + subcmd: u32, + sepol1: PolicyObject, + sepol2: PolicyObject, + sepol3: PolicyObject, + sepol4: PolicyObject, + sepol5: PolicyObject, + sepol6: PolicyObject, + sepol7: PolicyObject, +} + +impl<'a> TryFrom<&'a NormalPerm<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a NormalPerm<'a>) -> Result { + let mut result = vec![]; + let subcmd = match perm.op { + "allow" => 1, + "deny" => 2, + "auditallow" => 3, + "dontaudit" => 4, + _ => 0, + }; + for &s in &perm.source { + for &t in &perm.target { + for &c in &perm.class { + for &p in &perm.perm { + result.push(AtomicStatement { + cmd: CMD_NORMAL_PERM, + subcmd, + sepol1: s.try_into()?, + sepol2: t.try_into()?, + sepol3: c.try_into()?, + sepol4: p.try_into()?, + sepol5: PolicyObject::None, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }); + } + } + } + } + Ok(result) + } +} + +impl<'a> TryFrom<&'a XPerm<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a XPerm<'a>) -> Result { + let mut result = vec![]; + let subcmd = match perm.op { + "allowxperm" => 1, + "auditallowxperm" => 2, + "dontauditxperm" => 3, + _ => 0, + }; + for &s in &perm.source { + for &t in &perm.target { + for &c in &perm.class { + result.push(AtomicStatement { + cmd: CMD_XPERM, + subcmd, + sepol1: s.try_into()?, + sepol2: t.try_into()?, + sepol3: c.try_into()?, + sepol4: perm.operation.try_into()?, + sepol5: perm.perm_set.try_into()?, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }); + } + } + } + Ok(result) + } +} + +impl<'a> TryFrom<&'a TypeState<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a TypeState<'a>) -> Result { + let mut result = vec![]; + let subcmd = match perm.op { + "permissive" => 1, + "enforcing" => 2, + _ => 0, + }; + for &t in &perm.stype { + result.push(AtomicStatement { + cmd: CMD_TYPE_STATE, + subcmd, + sepol1: t.try_into()?, + sepol2: PolicyObject::None, + sepol3: PolicyObject::None, + sepol4: PolicyObject::None, + sepol5: PolicyObject::None, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }); + } + Ok(result) + } +} + +impl<'a> TryFrom<&'a Type<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a Type<'a>) -> Result { + let mut result = vec![]; + for &attr in &perm.attrs { + result.push(AtomicStatement { + cmd: CMD_TYPE, + subcmd: 0, + sepol1: perm.name.try_into()?, + sepol2: attr.try_into()?, + sepol3: PolicyObject::None, + sepol4: PolicyObject::None, + sepol5: PolicyObject::None, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }); + } + Ok(result) + } +} + +impl<'a> TryFrom<&'a TypeAttr<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a TypeAttr<'a>) -> Result { + let mut result = vec![]; + for &t in &perm.stype { + for &attr in &perm.sattr { + result.push(AtomicStatement { + cmd: CMD_TYPE_ATTR, + subcmd: 0, + sepol1: t.try_into()?, + sepol2: attr.try_into()?, + sepol3: PolicyObject::None, + sepol4: PolicyObject::None, + sepol5: PolicyObject::None, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }); + } + } + Ok(result) + } +} + +impl<'a> TryFrom<&'a Attr<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a Attr<'a>) -> Result { + let result = vec![AtomicStatement { + cmd: CMD_ATTR, + subcmd: 0, + sepol1: perm.name.try_into()?, + sepol2: PolicyObject::None, + sepol3: PolicyObject::None, + sepol4: PolicyObject::None, + sepol5: PolicyObject::None, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }]; + Ok(result) + } +} + +impl<'a> TryFrom<&'a TypeTransition<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a TypeTransition<'a>) -> Result { + let mut result = vec![]; + let obj = match perm.object_name { + Some(obj) => obj.try_into()?, + None => PolicyObject::None, + }; + result.push(AtomicStatement { + cmd: CMD_TYPE_TRANSITION, + subcmd: 0, + sepol1: perm.source.try_into()?, + sepol2: perm.target.try_into()?, + sepol3: perm.class.try_into()?, + sepol4: perm.default_type.try_into()?, + sepol5: obj, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }); + Ok(result) + } +} + +impl<'a> TryFrom<&'a TypeChange<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a TypeChange<'a>) -> Result { + let mut result = vec![]; + let subcmd = match perm.op { + "type_change" => 1, + "type_member" => 2, + _ => 0, + }; + result.push(AtomicStatement { + cmd: CMD_TYPE_CHANGE, + subcmd, + sepol1: perm.source.try_into()?, + sepol2: perm.target.try_into()?, + sepol3: perm.class.try_into()?, + sepol4: perm.default_type.try_into()?, + sepol5: PolicyObject::None, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }); + Ok(result) + } +} + +impl<'a> TryFrom<&'a GenFsCon<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(perm: &'a GenFsCon<'a>) -> Result { + let result = vec![AtomicStatement { + cmd: CMD_GENFSCON, + subcmd: 0, + sepol1: perm.fs_name.try_into()?, + sepol2: perm.partial_path.try_into()?, + sepol3: perm.fs_context.try_into()?, + sepol4: PolicyObject::None, + sepol5: PolicyObject::None, + sepol6: PolicyObject::None, + sepol7: PolicyObject::None, + }]; + Ok(result) + } +} + +impl<'a> TryFrom<&'a PolicyStatement<'a>> for Vec { + type Error = anyhow::Error; + fn try_from(value: &'a PolicyStatement) -> Result { + match value { + PolicyStatement::NormalPerm(perm) => perm.try_into(), + PolicyStatement::XPerm(perm) => perm.try_into(), + PolicyStatement::TypeState(perm) => perm.try_into(), + PolicyStatement::Type(perm) => perm.try_into(), + PolicyStatement::TypeAttr(perm) => perm.try_into(), + PolicyStatement::Attr(perm) => perm.try_into(), + PolicyStatement::TypeTransition(perm) => perm.try_into(), + PolicyStatement::TypeChange(perm) => perm.try_into(), + PolicyStatement::GenFsCon(perm) => perm.try_into(), + } + } +} + +//////////////////////////////////////////////////////////////// +/// for C FFI to call kernel interface +/////////////////////////////////////////////////////////////// + +#[derive(Debug)] +#[repr(C)] +struct FfiPolicy { + cmd: u32, + subcmd: u32, + sepol1: *const ffi::c_char, + sepol2: *const ffi::c_char, + sepol3: *const ffi::c_char, + sepol4: *const ffi::c_char, + sepol5: *const ffi::c_char, + sepol6: *const ffi::c_char, + sepol7: *const ffi::c_char, +} + +fn to_c_ptr(pol: &PolicyObject) -> *const ffi::c_char { + match pol { + PolicyObject::None | PolicyObject::All => std::ptr::null(), + PolicyObject::One(s) => s.as_ptr().cast::(), + } +} + +impl From for FfiPolicy { + fn from(policy: AtomicStatement) -> FfiPolicy { + FfiPolicy { + cmd: policy.cmd, + subcmd: policy.subcmd, + sepol1: to_c_ptr(&policy.sepol1), + sepol2: to_c_ptr(&policy.sepol2), + sepol3: to_c_ptr(&policy.sepol3), + sepol4: to_c_ptr(&policy.sepol4), + sepol5: to_c_ptr(&policy.sepol5), + sepol6: to_c_ptr(&policy.sepol6), + sepol7: to_c_ptr(&policy.sepol7), + } + } +} + +pub fn check_rule(policy: &str) -> Result<()> { + let path = Path::new(policy); + let policy = if path.exists() { + std::fs::read_to_string(path)? + } else { + policy.to_string() + }; + parse_sepolicy(policy.trim(), true)?; + Ok(()) +} diff --git a/apd/src/supercall.rs b/apd/src/supercall.rs new file mode 100644 index 0000000..b369f82 --- /dev/null +++ b/apd/src/supercall.rs @@ -0,0 +1,476 @@ +use crate::package::{read_ap_package_config, synchronize_package_uid}; +use errno::errno; +use libc::{EINVAL, c_int, c_long, c_void, execv, fork, pid_t, setenv, syscall, uid_t, wait}; +use log::{error, info, warn}; +use std::ffi::{CStr, CString}; +use std::fmt::Write; +use std::fs::File; +use std::io::{self, Read}; + +use std::process::exit; +use std::sync::{Arc, Mutex}; + +use std::{process, ptr}; + +const MAJOR: c_long = 0; +const MINOR: c_long = 11; +const PATCH: c_long = 1; + +const KSTORAGE_EXCLUDE_LIST_GROUP: i32 = 1; + +const __NR_SUPERCALL: c_long = 45; +const SUPERCALL_KLOG: c_long = 0x1004; +const SUPERCALL_KERNELPATCH_VER: c_long = 0x1008; +const SUPERCALL_KERNEL_VER: c_long = 0x1009; +const SUPERCALL_SU: c_long = 0x1010; +const SUPERCALL_KSTORAGE_WRITE: c_long = 0x1041; +const SUPERCALL_SU_GRANT_UID: c_long = 0x1100; +const SUPERCALL_SU_REVOKE_UID: c_long = 0x1101; +const SUPERCALL_SU_NUMS: c_long = 0x1102; +const SUPERCALL_SU_LIST: c_long = 0x1103; +const SUPERCALL_SU_RESET_PATH: c_long = 0x1111; +const SUPERCALL_SU_GET_SAFEMODE: c_long = 0x1112; + +const SUPERCALL_SCONTEXT_LEN: usize = 0x60; + +#[repr(C)] +struct SuProfile { + uid: i32, + to_uid: i32, + scontext: [u8; SUPERCALL_SCONTEXT_LEN], +} + +fn ver_and_cmd(cmd: c_long) -> c_long { + let version_code: u32 = ((MAJOR << 16) + (MINOR << 8) + PATCH).try_into().unwrap(); + ((version_code as c_long) << 32) | (0x1158 << 16) | (cmd & 0xFFFF) +} + +fn sc_su_revoke_uid(key: &CStr, uid: uid_t) -> c_long { + if key.to_bytes().is_empty() { + return (-EINVAL).into(); + } + unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_SU_REVOKE_UID), + uid, + ) as c_long + } +} + +fn sc_su_grant_uid(key: &CStr, profile: &SuProfile) -> c_long { + if key.to_bytes().is_empty() { + return (-EINVAL).into(); + } + unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_SU_GRANT_UID), + profile, + ) as c_long + } +} + +fn sc_kstorage_write( + key: &CStr, + gid: i32, + did: i64, + data: *mut c_void, + offset: i32, + dlen: i32, +) -> c_long { + if key.to_bytes().is_empty() { + return (-EINVAL).into(); + } + unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_KSTORAGE_WRITE), + gid as c_long, + did as c_long, + data, + (((offset as i64) << 32) | (dlen as i64)) as c_long, + ) as c_long + } +} + +fn sc_set_ap_mod_exclude(key: &CStr, uid: i64, exclude: i32) -> c_long { + sc_kstorage_write( + key, + KSTORAGE_EXCLUDE_LIST_GROUP, + uid, + &exclude as *const i32 as *mut c_void, + 0, + size_of::() as i32, + ) +} + +pub fn sc_su_get_safemode(key: &CStr) -> c_long { + if key.to_bytes().is_empty() { + warn!("[sc_su_get_safemode] null superkey, tell apd we are not in safemode!"); + return 0; + } + + let key_ptr = key.as_ptr(); + if key_ptr.is_null() { + warn!("[sc_su_get_safemode] superkey pointer is null!"); + return 0; + } + + unsafe { + syscall( + __NR_SUPERCALL, + key_ptr, + ver_and_cmd(SUPERCALL_SU_GET_SAFEMODE), + ) as c_long + } +} + +fn sc_su(key: &CStr, profile: &SuProfile) -> c_long { + if key.to_bytes().is_empty() { + return (-EINVAL).into(); + } + unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_SU), + profile, + ) as c_long + } +} + +fn sc_su_reset_path(key: &CStr, path: &CStr) -> c_long { + if key.to_bytes().is_empty() || path.to_bytes().is_empty() { + return (-EINVAL).into(); + } + unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_SU_RESET_PATH), + path.as_ptr(), + ) as c_long + } +} + +fn sc_kp_ver(key: &CStr) -> Result { + if key.to_bytes().is_empty() { + return Err(-EINVAL); + } + let ret = unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_KERNELPATCH_VER), + ) + }; + Ok(ret as u32) +} + +fn sc_k_ver(key: &CStr) -> Result { + if key.to_bytes().is_empty() { + return Err(-EINVAL); + } + let ret = unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_KERNEL_VER), + ) + }; + Ok(ret as u32) +} + +fn sc_klog(key: &CStr, msg: &CStr) -> c_long { + if key.to_bytes().is_empty() || msg.to_bytes().is_empty() { + return (-EINVAL).into(); + } + unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_KLOG), + msg.as_ptr(), + ) as c_long + } +} + +fn sc_su_uid_nums(key: &CStr) -> c_long { + if key.to_bytes().is_empty() { + return (-EINVAL).into(); + } + unsafe { syscall(__NR_SUPERCALL, key.as_ptr(), ver_and_cmd(SUPERCALL_SU_NUMS)) as c_long } +} + +fn sc_su_allow_uids(key: &CStr, buf: &mut [uid_t]) -> c_long { + if key.to_bytes().is_empty() { + return (-EINVAL).into(); + } + if buf.is_empty() { + return (-EINVAL).into(); + } + unsafe { + syscall( + __NR_SUPERCALL, + key.as_ptr(), + ver_and_cmd(SUPERCALL_SU_LIST), + buf.as_mut_ptr(), + buf.len() as i32, + ) as c_long + } +} + +fn read_file_to_string(path: &str) -> io::Result { + let mut file = File::open(path)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; + Ok(content) +} + +fn convert_string_to_u8_array(s: &str) -> [u8; SUPERCALL_SCONTEXT_LEN] { + let mut u8_array = [0u8; SUPERCALL_SCONTEXT_LEN]; + let bytes = s.as_bytes(); + let len = usize::min(SUPERCALL_SCONTEXT_LEN, bytes.len()); + u8_array[..len].copy_from_slice(&bytes[..len]); + u8_array +} + +fn convert_superkey(s: &Option) -> Option { + s.as_ref().and_then(|s| CString::new(s.clone()).ok()) +} + +pub fn refresh_ap_package_list(skey: &CStr, mutex: &Arc>) { + let _lock = mutex.lock().unwrap(); + + let num = sc_su_uid_nums(skey); + if num < 0 { + error!("[refresh_su_list] Error getting number of UIDs: {}", num); + return; + } + let num = num as usize; + let mut uids = vec![0 as uid_t; num]; + let n = sc_su_allow_uids(skey, &mut uids); + if n < 0 { + error!("[refresh_su_list] Error getting su list"); + return; + } + for uid in &uids { + if *uid == 0 || *uid == 2000 { + warn!( + "[refresh_ap_package_list] Skip revoking critical uid: {}", + uid + ); + continue; + } + info!( + "[refresh_ap_package_list] Revoking {} root permission...", + uid + ); + let rc = sc_su_revoke_uid(skey, *uid); + if rc != 0 { + error!("[refresh_ap_package_list] Error revoking UID: {}", rc); + } + } + + if let Err(e) = synchronize_package_uid() { + error!("Failed to synchronize package UIDs: {}", e); + } + + let package_configs = read_ap_package_config(); + for config in package_configs { + if config.allow == 1 && config.exclude == 0 { + let profile = SuProfile { + uid: config.uid, + to_uid: config.to_uid, + scontext: convert_string_to_u8_array(&config.sctx), + }; + let result = sc_su_grant_uid(skey, &profile); + info!( + "[refresh_ap_package_list] Loading {}: result = {}", + config.pkg, result + ); + } + if config.allow == 0 && config.exclude == 1 { + let result = sc_set_ap_mod_exclude(skey, config.uid as i64, 1); + info!( + "[refresh_ap_package_list] Loading exclude {}: result = {}", + config.pkg, result + ); + } + } +} + +pub fn privilege_apd_profile(superkey: &Option) { + let key = convert_superkey(superkey); + + let all_allow_ctx = "u:r:magisk:s0"; + let profile = SuProfile { + uid: process::id().try_into().expect("PID conversion failed"), + to_uid: 0, + scontext: convert_string_to_u8_array(all_allow_ctx), + }; + if let Some(ref key) = key { + let result = sc_su(key, &profile); + info!("[privilege_apd_profile] result = {}", result); + } +} + +pub fn init_load_package_uid_config(superkey: &Option) { + let package_configs = read_ap_package_config(); + let key = convert_superkey(superkey); + + for config in package_configs { + if config.allow == 1 && config.exclude == 0 { + match key { + Some(ref key) => { + let profile = SuProfile { + uid: config.uid, + to_uid: config.to_uid, + scontext: convert_string_to_u8_array(&config.sctx), + }; + let result = sc_su_grant_uid(key, &profile); + info!("Processed {}: result = {}", config.pkg, result); + } + _ => { + warn!("Superkey is None, skipping config: {}", config.pkg); + } + } + } + if config.allow == 0 && config.exclude == 1 { + match key { + Some(ref key) => { + let result = sc_set_ap_mod_exclude(key, config.uid as i64, 1); + info!("Processed exclude {}: result = {}", config.pkg, result); + } + _ => { + warn!("Superkey is None, skipping config: {}", config.pkg); + } + } + } + } +} + +pub fn init_load_su_path(superkey: &Option) { + let su_path_file = "/data/adb/ap/su_path"; + + match read_file_to_string(su_path_file) { + Ok(su_path) => { + let superkey_cstr = convert_superkey(superkey); + + match superkey_cstr { + Some(superkey_cstr) => match CString::new(su_path.trim()) { + Ok(su_path_cstr) => { + let result = sc_su_reset_path(&superkey_cstr, &su_path_cstr); + if result == 0 { + info!("suPath load successfully"); + } else { + warn!("Failed to load su path, error code: {}", result); + } + } + Err(e) => { + warn!("Failed to convert su_path: {}", e); + } + }, + _ => { + warn!("Superkey is None, skipping..."); + } + } + } + Err(e) => { + warn!("Failed to read su_path file: {}", e); + } + } +} + +fn set_env_var(key: &str, value: &str) { + let key_c = CString::new(key).expect("CString::new failed"); + let value_c = CString::new(value).expect("CString::new failed"); + unsafe { + setenv(key_c.as_ptr(), value_c.as_ptr(), 1); + } +} + +fn log_kernel(key: &CStr, _fmt: &str, args: std::fmt::Arguments) -> c_long { + let mut buf = String::with_capacity(1024); + write!(&mut buf, "{}", args).expect("Error formatting string"); + + let c_buf = CString::new(buf).expect("CString::new failed"); + sc_klog(key, &c_buf) +} + +#[macro_export] +macro_rules! log_kernel { + ($key:expr_2021, $fmt:expr_2021, $($arg:tt)*) => ( + log_kernel($key, $fmt, std::format_args!($fmt, $($arg)*)) + ) +} + +pub fn fork_for_result(exec: &str, argv: &[&str], key: &Option) { + let mut cmd = String::new(); + for arg in argv { + cmd.push_str(arg); + cmd.push(' '); + } + + let superkey_cstr = convert_superkey(key); + + match superkey_cstr { + Some(superkey_cstr) => { + unsafe { + let pid: pid_t = fork(); + if pid < 0 { + log_kernel!( + &superkey_cstr, + "{} fork {} error: {}\n", + libc::getpid(), + exec, + -1 + ); + } else if pid == 0 { + set_env_var("KERNELPATCH", "true"); + let kpver = format!("{:x}", sc_kp_ver(&superkey_cstr).unwrap_or(0)); + set_env_var("KERNELPATCH_VERSION", kpver.as_str()); + let kver = format!("{:x}", sc_k_ver(&superkey_cstr).unwrap_or(0)); + set_env_var("KERNEL_VERSION", kver.as_str()); + + let c_exec = CString::new(exec).expect("CString::new failed"); + let c_argv: Vec = + argv.iter().map(|&arg| CString::new(arg).unwrap()).collect(); + let mut c_argv_ptrs: Vec<*const libc::c_char> = + c_argv.iter().map(|arg| arg.as_ptr()).collect(); + c_argv_ptrs.push(ptr::null()); + + execv(c_exec.as_ptr(), c_argv_ptrs.as_ptr()); + + log_kernel!( + &superkey_cstr, + "{} exec {} error: {}\n", + libc::getpid(), + cmd, + CStr::from_ptr(libc::strerror(errno().0)) + .to_string_lossy() + .into_owned() + ); + exit(1); // execv only returns on error + } else { + let mut status: c_int = 0; + wait(&mut status); + log_kernel!( + &superkey_cstr, + "{} wait {} status: 0x{}\n", + libc::getpid(), + cmd, + status + ); + } + } + } + _ => { + warn!("[fork_for_result] SuperKey convert failed!"); + } + } +} diff --git a/apd/src/utils.rs b/apd/src/utils.rs new file mode 100644 index 0000000..653c26d --- /dev/null +++ b/apd/src/utils.rs @@ -0,0 +1,198 @@ +use anyhow::{Context, Error, Ok, Result, bail}; +use log::{info, warn}; +use std::ffi::CString; +use std::{ + fs::{File, OpenOptions, create_dir_all}, + io::{BufRead, BufReader, ErrorKind::AlreadyExists, Write}, + path::Path, + process::Stdio, +}; + +use crate::defs; +use std::fs::metadata; +#[allow(unused_imports)] +use std::fs::{Permissions, set_permissions}; +#[cfg(unix)] +use std::os::unix::prelude::PermissionsExt; +use std::process::Command; + +use crate::supercall::sc_su_get_safemode; + +pub fn ensure_clean_dir(dir: &str) -> Result<()> { + let path = Path::new(dir); + log::debug!("ensure_clean_dir: {}", path.display()); + if path.exists() { + log::debug!("ensure_clean_dir: {} exists, remove it", path.display()); + std::fs::remove_dir_all(path)?; + } + Ok(create_dir_all(path)?) +} + +pub fn ensure_file_exists>(file: T) -> Result<()> { + match File::options().write(true).create_new(true).open(&file) { + Result::Ok(_) => Ok(()), + Err(err) => { + if err.kind() == AlreadyExists && file.as_ref().is_file() { + Ok(()) + } else { + Err(Error::from(err)) + .with_context(|| format!("{} is not a regular file", file.as_ref().display())) + } + } + } +} + +pub fn ensure_dir_exists>(dir: T) -> Result<()> { + let result = create_dir_all(&dir).map_err(Error::from); + if dir.as_ref().is_dir() { + result + } else if result.is_ok() { + bail!("{} is not a regular directory", dir.as_ref().display()) + } else { + result + } +} + +// todo: ensure +pub fn ensure_binary>(path: T) -> Result<()> { + set_permissions(&path, Permissions::from_mode(0o755))?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn getprop(prop: &str) -> Option { + android_properties::getprop(prop).value() +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn getprop(_prop: &str) -> Option { + unimplemented!() +} +pub fn run_command( + command: &str, + args: &[&str], + stdout: Option, +) -> Result { + let mut command_builder = Command::new(command); + command_builder.args(args); + if let Some(out) = stdout { + command_builder.stdout(out); + } + let child = command_builder.spawn()?; + Ok(child) +} +pub fn is_safe_mode(superkey: Option) -> bool { + let safemode = getprop("persist.sys.safemode") + .filter(|prop| prop == "1") + .is_some() + || getprop("ro.sys.safemode") + .filter(|prop| prop == "1") + .is_some(); + info!("safemode: {}", safemode); + if safemode { + return true; + } + let safemode = superkey + .as_ref() + .and_then(|key_str| CString::new(key_str.as_str()).ok()) + .map_or_else( + || { + warn!("[is_safe_mode] No valid superkey provided, assuming safemode as false."); + false + }, + |cstr| sc_su_get_safemode(&cstr) == 1, + ); + info!("kernel_safemode: {}", safemode); + safemode +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn switch_mnt_ns(pid: i32) -> Result<()> { + use anyhow::ensure; + use std::os::fd::AsRawFd; + let path = format!("/proc/{pid}/ns/mnt"); + let fd = File::open(path)?; + let current_dir = std::env::current_dir(); + let ret = unsafe { libc::setns(fd.as_raw_fd(), libc::CLONE_NEWNS) }; + if let Result::Ok(current_dir) = current_dir { + let _ = std::env::set_current_dir(current_dir); + } + ensure!(ret == 0, "switch mnt ns failed"); + Ok(()) +} + +pub fn is_overlayfs_supported() -> Result { + let file = + File::open("/proc/filesystems").with_context(|| "Failed to open /proc/filesystems")?; + let reader = BufReader::new(file); + + let overlay_supported = reader.lines().any(|line| { + if let Result::Ok(line) = line { + line.contains("overlay") + } else { + false + } + }); + + Ok(overlay_supported) +} + +pub fn should_use_overlayfs() -> Result { + let force_using_overlayfs = Path::new(defs::FORCE_OVERLAYFS_FILE).exists(); + let overlayfs_supported = is_overlayfs_supported()?; + + Ok(force_using_overlayfs && overlayfs_supported) +} + +fn switch_cgroup(grp: &str, pid: u32) { + let path = Path::new(grp).join("cgroup.procs"); + if !path.exists() { + return; + } + + let fp = OpenOptions::new().append(true).open(path); + if let Result::Ok(mut fp) = fp { + let _ = write!(fp, "{pid}"); + } +} + +pub fn switch_cgroups() { + let pid = std::process::id(); + switch_cgroup("/acct", pid); + switch_cgroup("/dev/cg2_bpf", pid); + switch_cgroup("/sys/fs/cgroup", pid); + + if getprop("ro.config.per_app_memcg") + .filter(|prop| prop == "false") + .is_none() + { + switch_cgroup("/dev/memcg/apps", pid); + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn umask(mask: u32) { + unsafe { libc::umask(mask) }; +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn umask(_mask: u32) { + unimplemented!("umask is not supported on this platform") +} + +pub fn has_magisk() -> bool { + which::which("magisk").is_ok() +} +pub fn get_tmp_path() -> &'static str { + if metadata(defs::TEMP_DIR_LEGACY).is_ok() { + return defs::TEMP_DIR_LEGACY; + } + if metadata(defs::TEMP_DIR).is_ok() { + return defs::TEMP_DIR; + } + "" +} +pub fn get_work_dir() -> String { + let tmp_path = get_tmp_path(); + format!("{}/workdir/", tmp_path) +} diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..dc5ca96 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build +/release/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..2d6ee83 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,294 @@ +@file:Suppress("UnstableApiUsage") + +import com.android.build.gradle.tasks.PackageAndroidArtifact +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import java.net.URI + +plugins { + alias(libs.plugins.agp.app) + alias(libs.plugins.kotlin) + alias(libs.plugins.kotlin.compose.compiler) + alias(libs.plugins.ksp) + alias(libs.plugins.lsplugin.apksign) + alias(libs.plugins.lsplugin.resopt) + alias(libs.plugins.lsplugin.cmaker) + id("kotlin-parcelize") +} + +val managerVersionCode: Int by rootProject.extra +val managerVersionName: String by rootProject.extra +val branchname: String by rootProject.extra +val kernelPatchVersion: String by rootProject.extra + +apksign { + storeFileProperty = "KEYSTORE_FILE" + storePasswordProperty = "KEYSTORE_PASSWORD" + keyAliasProperty = "KEY_ALIAS" + keyPasswordProperty = "KEY_PASSWORD" +} + +android { + namespace = "me.bmax.apatch" + + buildTypes { + debug { + isDebuggable = true + isMinifyEnabled = false + isShrinkResources = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + release { + isMinifyEnabled = true + isShrinkResources = true + isDebuggable = false + multiDexEnabled = true + vcsInfo.include = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + dependenciesInfo.includeInApk = false + + // https://stackoverflow.com/a/77745844 + tasks.withType { + doFirst { appMetadata.asFile.orNull?.writeText("") } + } + + buildFeatures { + aidl = true + buildConfig = true + compose = true + prefab = true + } + + defaultConfig { + buildConfigField("String", "buildKPV", "\"$kernelPatchVersion\"") + + base.archivesName = "APatch_${managerVersionCode}_${managerVersionName}_on_${branchname}" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + packaging { + jniLibs { + useLegacyPackaging = true + } + resources { + excludes += "**" + merges += "META-INF/com/google/android/**" + } + } + + externalNativeBuild { + cmake { + version = "3.28.0+" + path("src/main/cpp/CMakeLists.txt") + } + } + + androidResources { + generateLocaleConfig = true + } + + sourceSets["main"].jniLibs.srcDir("libs") + + applicationVariants.all { + kotlin.sourceSets { + getByName(name) { + kotlin.srcDir("build/generated/ksp/$name/kotlin") + } + } + } +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +kotlin { + jvmToolchain(21) + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } +} + +fun registerDownloadTask( + taskName: String, srcUrl: String, destPath: String, project: Project +) { + project.tasks.register(taskName) { + val destFile = File(destPath) + + doLast { + if (!destFile.exists() || isFileUpdated(srcUrl, destFile)) { + println(" - Downloading $srcUrl to ${destFile.absolutePath}") + downloadFile(srcUrl, destFile) + println(" - Download completed.") + } else { + println(" - File is up-to-date, skipping download.") + } + } + } +} + +fun isFileUpdated(url: String, localFile: File): Boolean { + val connection = URI.create(url).toURL().openConnection() + val remoteLastModified = connection.getHeaderFieldDate("Last-Modified", 0L) + return remoteLastModified > localFile.lastModified() +} + +fun downloadFile(url: String, destFile: File) { + URI.create(url).toURL().openStream().use { input -> + destFile.outputStream().use { output -> + input.copyTo(output) + } + } +} + +registerDownloadTask( + taskName = "downloadKpimg", + srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/$kernelPatchVersion/kpimg-android", + destPath = "${project.projectDir}/src/main/assets/kpimg", + project = project +) + +registerDownloadTask( + taskName = "downloadKptools", + srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/$kernelPatchVersion/kptools-android", + destPath = "${project.projectDir}/libs/arm64-v8a/libkptools.so", + project = project +) + +// Compat kp version less than 0.10.7 +// TODO: Remove in future +registerDownloadTask( + taskName = "downloadCompatKpatch", + srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/0.10.7/kpatch-android", + destPath = "${project.projectDir}/libs/arm64-v8a/libkpatch.so", + project = project +) + +tasks.register("mergeScripts") { + into("${project.projectDir}/src/main/resources/META-INF/com/google/android") + from(rootProject.file("${project.rootDir}/scripts/update_binary.sh")) { + rename { "update-binary" } + } + from(rootProject.file("${project.rootDir}/scripts/update_script.sh")) { + rename { "updater-script" } + } +} + +tasks.getByName("preBuild").dependsOn( + "downloadKpimg", + "downloadKptools", + "downloadCompatKpatch", + "mergeScripts", +) + +// https://github.com/bbqsrc/cargo-ndk +// cargo ndk -t arm64-v8a build --release +tasks.register("cargoBuild") { + executable("cargo") + args("ndk", "-t", "arm64-v8a", "build", "--release") + workingDir("${project.rootDir}/apd") +} + +tasks.register("buildApd") { + dependsOn("cargoBuild") + from("${project.rootDir}/apd/target/aarch64-linux-android/release/apd") + into("${project.projectDir}/libs/arm64-v8a") + rename("apd", "libapd.so") +} + +tasks.configureEach { + if (name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders") { + dependsOn("buildApd") + } +} + +tasks.register("cargoClean") { + executable("cargo") + args("clean") + workingDir("${project.rootDir}/apd") +} + +tasks.register("apdClean") { + dependsOn("cargoClean") + delete(file("${project.projectDir}/libs/arm64-v8a/libapd.so")) +} + +tasks.clean { + dependsOn("apdClean") +} + +ksp { + arg("compose-destinations.defaultTransitions", "none") +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.webkit) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.material) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.runtime.livedata) + + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) + + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + + implementation(libs.compose.destinations.core) + ksp(libs.compose.destinations.ksp) + + implementation(libs.com.github.topjohnwu.libsu.core) + implementation(libs.com.github.topjohnwu.libsu.service) + implementation(libs.com.github.topjohnwu.libsu.nio) + implementation(libs.com.github.topjohnwu.libsu.io) + + implementation(libs.dev.rikka.rikkax.parcelablelist) + + implementation(libs.io.coil.kt.coil.compose) + + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.me.zhanghai.android.appiconloader.coil) + + implementation(libs.sheet.compose.dialogs.core) + implementation(libs.sheet.compose.dialogs.list) + implementation(libs.sheet.compose.dialogs.input) + + implementation(libs.markdown) + + implementation(libs.ini4j) + + compileOnly(libs.cxx) +} + +cmaker { + default { + arguments += "-DANDROID_STL=none" + arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters("arm64-v8a") + cppFlags += "-std=c++2b" + cFlags += "-std=c2x" + } +} diff --git a/app/libs/arm64-v8a/.gitignore b/app/libs/arm64-v8a/.gitignore new file mode 100644 index 0000000..2ba96b7 --- /dev/null +++ b/app/libs/arm64-v8a/.gitignore @@ -0,0 +1,4 @@ +libkptools.so +libapjni.so +libkpatch.so +libapd.so \ No newline at end of file diff --git a/app/libs/arm64-v8a/libbootctl.so b/app/libs/arm64-v8a/libbootctl.so new file mode 100755 index 0000000..cf5c613 Binary files /dev/null and b/app/libs/arm64-v8a/libbootctl.so differ diff --git a/app/libs/arm64-v8a/libbusybox.so b/app/libs/arm64-v8a/libbusybox.so new file mode 100755 index 0000000..2fd8bcf Binary files /dev/null and b/app/libs/arm64-v8a/libbusybox.so differ diff --git a/app/libs/arm64-v8a/libmagiskboot.so b/app/libs/arm64-v8a/libmagiskboot.so new file mode 100755 index 0000000..5f9e97c Binary files /dev/null and b/app/libs/arm64-v8a/libmagiskboot.so differ diff --git a/app/libs/arm64-v8a/libmagiskpolicy.so b/app/libs/arm64-v8a/libmagiskpolicy.so new file mode 100755 index 0000000..99524a7 Binary files /dev/null and b/app/libs/arm64-v8a/libmagiskpolicy.so differ diff --git a/app/libs/arm64-v8a/libresetprop.so b/app/libs/arm64-v8a/libresetprop.so new file mode 100755 index 0000000..3498c13 Binary files /dev/null and b/app/libs/arm64-v8a/libresetprop.so differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..54bbae2 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,26 @@ +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.conscrypt.ConscryptHostnameVerifier +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE +-dontwarn java.beans.Introspector +-dontwarn java.beans.VetoableChangeListener +-dontwarn java.beans.VetoableChangeSupport + +# Keep ini4j Service Provider Interface +-keep,allowobfuscation,allowoptimization public class org.ini4j.spi.* + +# Kotlin +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + public static void check*(...); + public static void throw*(...); +} + +-repackageclasses +-allowaccessmodification +-overloadaggressively +-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8a70f04 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/aidl/me/bmax/apatch/IAPRootService.aidl b/app/src/main/aidl/me/bmax/apatch/IAPRootService.aidl new file mode 100644 index 0000000..471b7ed --- /dev/null +++ b/app/src/main/aidl/me/bmax/apatch/IAPRootService.aidl @@ -0,0 +1,9 @@ +// IAPRootService.aidl +package me.bmax.apatch; + +import android.content.pm.PackageInfo; +import rikka.parcelablelist.ParcelableListSlice; + +interface IAPRootService { + ParcelableListSlice getPackages(int flags); +} \ No newline at end of file diff --git a/app/src/main/assets/.gitignore b/app/src/main/assets/.gitignore new file mode 100644 index 0000000..891e15f --- /dev/null +++ b/app/src/main/assets/.gitignore @@ -0,0 +1,2 @@ +kpimg +*.kpm \ No newline at end of file diff --git a/app/src/main/assets/InstallAP.sh b/app/src/main/assets/InstallAP.sh new file mode 100644 index 0000000..2f5dd94 --- /dev/null +++ b/app/src/main/assets/InstallAP.sh @@ -0,0 +1,116 @@ +#!/bin/sh +# By SakuraKyuo + +OUTFD=/proc/self/fd/$2 + +function ui_print() { + echo -e "ui_print $1\nui_print" >> $OUTFD +} + +function ui_printfile() { + while IFS='' read -r line || $BB [[ -n "$line" ]]; do + ui_print "$line"; + done < $1; +} + +function kernelFlagsErr(){ + ui_print "- Installation has Aborted!" + ui_print "- APatch requires CONFIG_KALLSYMS to be Enabled." + ui_print "- But your kernel seems NOT enabled it." + exit +} + +function apatchNote(){ + ui_print "- APatch Patch Done" + ui_print "- APatch Key is: Ap$skey" + ui_print "- We do have saved Origin Boot image to /data" + ui_print "- If you encounter bootloop, reboot into Recovery and flash it" + exit +} + +function failed(){ + ui_printfile /dev/tmp/install/log + ui_print "- APatch Patch Failed." + ui_print "- Please feedback to the developer with the screenshots." + exit +} + +function boot_execute_ab(){ + ./lib/arm64-v8a/libmagiskboot.so unpack boot.img + if [[ ! $(./lib/arm64-v8a/libkptools.so -i ./kernel -f | grep CONFIG_KALLSYMS=y) ]]; then + kernelFlagsErr + fi + mv kernel kernel-origin + ./lib/arm64-v8a/libkptools.so -p --image kernel-origin --skey "Ap$skey" --kpimg ./assets/kpimg --out ./kernel 2>&1 | tee /dev/tmp/install/log + if [[ ! $(cat /dev/tmp/install/log | grep "patch done") ]]; then + failed + fi + ui_printfile /dev/tmp/install/log + ./lib/arm64-v8a/libmagiskboot.so repack boot.img + dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot + mv boot.img /data/boot.img + apatchNote +} + +function boot_execute(){ + ./lib/arm64-v8a/libmagiskboot.so unpack boot.img + if [[ ! $(./lib/arm64-v8a/libkptools.so -i ./kernel -f | grep CONFIG_KALLSYMS=y) ]]; then + kernelFlagsErr + fi + mv kernel kernel-origin + ./lib/arm64-v8a/libkptools.so -p --image kernel-origin --skey "Ap$skey" --kpimg ./assets/kpimg --out ./kernel 2>&1 | tee /dev/tmp/install/log + if [[ ! $(cat /dev/tmp/install/log | grep "patch done") ]]; then + failed + fi + ui_printfile /dev/tmp/install/log + ./lib/arm64-v8a/libmagiskboot.so repack boot.img + dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot + mv boot.img /data/boot.img + apatchNote +} + +function main(){ + +cd /dev/tmp/install + +chmod a+x ./assets/kpimg +chmod a+x ./lib/arm64-v8a/libkptools.so +chmod a+x ./lib/arm64-v8a/libmagiskboot.so + +slot=$(getprop ro.boot.slot_suffix) + +skey=$(cat /proc/sys/kernel/random/uuid | cut -d \- -f1) + +if [[ ! "$slot" == "" ]]; then + + ui_print "" + ui_print "- You are using A/B device." + + # Script author + ui_print "- Install Script by SakuraKyuo" + + # Get kernel + ui_print "" + dd if=/dev/block/by-name/boot$slot of=/dev/tmp/install/boot.img + if [[ "$?" == 0 ]]; then + ui_print "- Detected boot partition." + boot_execute_ab + fi + +else + + ui_print "You are using A Only device." + + # Get kernel + ui_print "" + dd if=/dev/block/by-name/boot of=/dev/tmp/install/boot.img + if [[ "$?" == 0 ]]; then + ui_print "- Detected boot partition." + boot_execute + fi + +fi + +} + +main diff --git a/app/src/main/assets/UninstallAP.sh b/app/src/main/assets/UninstallAP.sh new file mode 100644 index 0000000..2ea16cf --- /dev/null +++ b/app/src/main/assets/UninstallAP.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# By SakuraKyuo + +OUTFD=/proc/self/fd/$2 + +function ui_print() { + echo -e "ui_print $1\nui_print" >> $OUTFD +} + +function ui_printfile() { + while IFS='' read -r line || $BB [[ -n "$line" ]]; do + ui_print "$line"; + done < $1; +} + +function apatchNote(){ + ui_print "- APatch Unpatch Done" + exit +} + +function failed(){ + ui_print "- APatch Unpatch Failed." + ui_print "- Please feedback to the developer with the screenshots." + exit +} + +function boot_execute_ab(){ + ./lib/arm64-v8a/libmagiskboot.so unpack boot.img + mv kernel kernel-origin + ./lib/arm64-v8a/libkptools.so -u --image kernel-origin --out ./kernel + if [[ ! "$?" == 0 ]]; then + failed + fi + ./lib/arm64-v8a/libmagiskboot.so repack boot.img + dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot + apatchNote +} + +function boot_execute(){ + ./lib/arm64-v8a/libmagiskboot.so unpack boot.img + mv kernel kernel-origin + ./lib/arm64-v8a/libkptools.so -u --image kernel-origin --out ./kernel + if [[ ! "$?" == 0 ]]; then + failed + fi + ./lib/arm64-v8a/libmagiskboot.so repack boot.img + dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot + apatchNote +} + +function main(){ + +cd /dev/tmp/install + +chmod a+x ./lib/arm64-v8a/libkptools.so +chmod a+x ./lib/arm64-v8a/libmagiskboot.so + +slot=$(getprop ro.boot.slot_suffix) + +if [[ ! "$slot" == "" ]]; then + + ui_print "" + ui_print "- You are using A/B device." + + # Get kernel + ui_print "" + dd if=/dev/block/by-name/boot$slot of=/dev/tmp/install/boot.img + if [[ "$?" == 0 ]]; then + ui_print "- Detected boot partition." + boot_execute_ab + fi + +else + + ui_print "You are using A Only device." + + # Get kernel + ui_print "" + dd if=/dev/block/by-name/boot of=/dev/tmp/install/boot.img + if [[ "$?" == 0 ]]; then + ui_print "- Detected boot partition." + boot_execute + fi + +fi + +} + +main \ No newline at end of file diff --git a/app/src/main/assets/boot_extract.sh b/app/src/main/assets/boot_extract.sh new file mode 100644 index 0000000..252aa2c --- /dev/null +++ b/app/src/main/assets/boot_extract.sh @@ -0,0 +1,20 @@ +#!/system/bin/sh + +ARCH=$(getprop ro.product.cpu.abi) + +IS_INSTALL_NEXT_SLOT=$1 + +# Load utility functions +. ./util_functions.sh + +if [ "$IS_INSTALL_NEXT_SLOT" = "true" ]; then + get_next_slot +else + get_current_slot +fi + +find_boot_image + +[ -e "$BOOTIMAGE" ] || { >&2 echo "- can't find boot.img!"; exit 1; } + +true diff --git a/app/src/main/assets/boot_patch.sh b/app/src/main/assets/boot_patch.sh new file mode 100644 index 0000000..17dbc46 --- /dev/null +++ b/app/src/main/assets/boot_patch.sh @@ -0,0 +1,107 @@ +#!/system/bin/sh +####################################################################################### +# APatch Boot Image Patcher +####################################################################################### +# +# Usage: boot_patch.sh [ARGS_PASS_TO_KPTOOLS] +# +# This script should be placed in a directory with the following files: +# +# File name Type Description +# +# boot_patch.sh script A script to patch boot image for APatch. +# (this file) The script will use files in its same +# directory to complete the patching process. +# bootimg binary The target boot image +# kpimg binary KernelPatch core Image +# kptools executable The KernelPatch tools binary to inject kpimg to kernel Image +# magiskboot executable Magisk tool to unpack boot.img. +# +####################################################################################### + +ARCH=$(getprop ro.product.cpu.abi) + +# Load utility functions +. ./util_functions.sh + +echo "****************************" +echo " APatch Boot Image Patcher" +echo "****************************" + +SUPERKEY="$1" +BOOTIMAGE=$2 +FLASH_TO_DEVICE=$3 +shift 2 + +[ -z "$SUPERKEY" ] && { >&2 echo "- SuperKey empty!"; exit 1; } +[ -e "$BOOTIMAGE" ] || { >&2 echo "- $BOOTIMAGE does not exist!"; exit 1; } + +# Check for dependencies +command -v ./magiskboot >/dev/null 2>&1 || { >&2 echo "- Command magiskboot not found!"; exit 1; } +command -v ./kptools >/dev/null 2>&1 || { >&2 echo "- Command kptools not found!"; exit 1; } + +if [ ! -f kernel ]; then +echo "- Unpacking boot image" +./magiskboot unpack "$BOOTIMAGE" >/dev/null 2>&1 + if [ $? -ne 0 ]; then + >&2 echo "- Unpack error: $?" + exit $? + fi +fi + +if [ ! $(./kptools -i kernel -f | grep CONFIG_KALLSYMS=y) ]; then + echo "- Patcher has Aborted!" + echo "- APatch requires CONFIG_KALLSYMS to be Enabled." + echo "- But your kernel seems NOT enabled it." + exit 0 +fi + +if [ $(./kptools -i kernel -l | grep patched=false) ]; then + echo "- Backing boot.img " + cp "$BOOTIMAGE" "ori.img" >/dev/null 2>&1 +fi + +mv kernel kernel.ori + +echo "- Patching kernel" + +set -x +./kptools -p -i kernel.ori -S "$SUPERKEY" -k kpimg -o kernel "$@" +patch_rc=$? +set +x + +if [ $patch_rc -ne 0 ]; then + >&2 echo "- Patch kernel error: $patch_rc" + exit $? +fi + +echo "- Repacking boot image" +./magiskboot repack "$BOOTIMAGE" >/dev/null 2>&1 + +if [ ! $(./kptools -i kernel.ori -f | grep CONFIG_KALLSYMS_ALL=y) ]; then + echo "- Detected CONFIG_KALLSYMS_ALL is not set!" + echo "- APatch has patched but maybe your device won't boot." + echo "- Make sure you have original boot image backup." +fi + +if [ $? -ne 0 ]; then + >&2 echo "- Repack error: $?" + exit $? +fi + +if [ "$FLASH_TO_DEVICE" = "true" ]; then + # flash + if [ -b "$BOOTIMAGE" ] || [ -c "$BOOTIMAGE" ] && [ -f "new-boot.img" ]; then + echo "- Flashing new boot image" + flash_image new-boot.img "$BOOTIMAGE" + if [ $? -ne 0 ]; then + >&2 echo "- Flash error: $?" + exit $? + fi + fi + + echo "- Successfully Flashed!" +else + echo "- Successfully Patched!" +fi + diff --git a/app/src/main/assets/boot_unpatch.sh b/app/src/main/assets/boot_unpatch.sh new file mode 100644 index 0000000..4eb79dd --- /dev/null +++ b/app/src/main/assets/boot_unpatch.sh @@ -0,0 +1,74 @@ +#!/system/bin/sh +####################################################################################### +# APatch Boot Image Unpatcher +####################################################################################### + +ARCH=$(getprop ro.product.cpu.abi) + +# Load utility functions +. ./util_functions.sh + +echo "****************************" +echo " APatch Boot Image Unpatcher" +echo "****************************" + +BOOTIMAGE=$1 + +[ -e "$BOOTIMAGE" ] || { echo "- $BOOTIMAGE does not exist!"; exit 1; } + +echo "- Target image: $BOOTIMAGE" + + # Check for dependencies +command -v ./magiskboot >/dev/null 2>&1 || { echo "- Command magiskboot not found!"; exit 1; } +command -v ./kptools >/dev/null 2>&1 || { echo "- Command kptools not found!"; exit 1; } + +if [ ! -f kernel ]; then +echo "- Unpacking boot image" +./magiskboot unpack "$BOOTIMAGE" >/dev/null 2>&1 +if [ $? -ne 0 ]; then + >&2 echo "- Unpack error: $?" + exit $? + fi +fi + +if [ ! $(./kptools -i kernel -l | grep patched=false) ]; then + echo "- kernel has been patched " + if [ -f "new-boot.img" ]; then + echo "- found backup boot.img ,use it for recovery" + else + mv kernel kernel.ori + echo "- Unpatching kernel" + ./kptools -u --image kernel.ori --out kernel + if [ $? -ne 0 ]; then + >&2 echo "- Unpatch error: $?" + exit $? + fi + echo "- Repacking boot image" + ./magiskboot repack "$BOOTIMAGE" >/dev/null 2>&1 + if [ $? -ne 0 ]; then + >&2 echo "- Repack error: $?" + exit $? + fi + fi + +else + echo "- no need unpatch" + exit 0 +fi + + + +if [ -f "new-boot.img" ]; then + echo "- Flashing boot image" + flash_image new-boot.img "$BOOTIMAGE" + + if [ $? -ne 0 ]; then + >&2 echo "- Flash error: $?" + exit $? + fi +fi + +echo "- Flash successful" + +# Reset any error code +true diff --git a/app/src/main/assets/util_functions.sh b/app/src/main/assets/util_functions.sh new file mode 100644 index 0000000..0d2a18c --- /dev/null +++ b/app/src/main/assets/util_functions.sh @@ -0,0 +1,537 @@ +#!/system/bin/sh +####################################################################################### +# Helper Functions (credits to topjohnwu) +####################################################################################### +APATCH_VER='0.10.4' +APATCH_VER_CODE=164 + +ui_print() { + if $BOOTMODE; then + echo "$1" + else + echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD + fi +} + +toupper() { + echo "$@" | tr '[:lower:]' '[:upper:]' +} + +grep_cmdline() { + local REGEX="s/^$1=//p" + { echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \ + sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \ + } 2>/dev/null | sed -n "$REGEX" +} + +grep_prop() { + local REGEX="s/^$1=//p" + shift + local FILES=$@ + [ -z "$FILES" ] && FILES='/system/build.prop' + cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1 +} + +getvar() { + local VARNAME=$1 + local VALUE + local PROPPATH='/data/.magisk /cache/.magisk' + [ ! -z $MAGISKTMP ] && PROPPATH="$MAGISKTMP/.magisk/config $PROPPATH" + VALUE=$(grep_prop $VARNAME $PROPPATH) + [ ! -z $VALUE ] && eval $VARNAME=\$VALUE +} + +is_mounted() { + grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null + return $? +} +abort() { + ui_print "$1" + $BOOTMODE || recovery_cleanup + [ ! -z $MODPATH ] && rm -rf $MODPATH + rm -rf $TMPDIR + exit 1 +} +set_nvbase() { + NVBASE="$1" + MAGISKBIN="$1/magisk" +} + +print_title() { + local len line1len line2len bar + line1len=$(echo -n $1 | wc -c) + line2len=$(echo -n $2 | wc -c) + len=$line2len + [ $line1len -gt $line2len ] && len=$line1len + len=$((len + 2)) + bar=$(printf "%${len}s" | tr ' ' '*') + ui_print "$bar" + ui_print " $1 " + [ "$2" ] && ui_print " $2 " + ui_print "$bar" +} +setup_flashable() { + ensure_bb + $BOOTMODE && return + if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then + # We will have to manually find out OUTFD + for FD in $(ls /proc/$$/fd); do + if readlink /proc/$$/fd/$FD | grep -q pipe; then + if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then + OUTFD=$FD + break + fi + fi + done + fi + recovery_actions +} + +ensure_bb() { + if set -o | grep -q standalone; then + # We are definitely in busybox ash + set -o standalone + return + fi + + # Find our busybox binary + local bb + if [ -f $TMPDIR/busybox ]; then + bb=$TMPDIR/busybox + elif [ -f $MAGISKBIN/busybox ]; then + bb=$MAGISKBIN/busybox + else + abort "! Cannot find BusyBox" + fi + chmod 755 $bb + + # Busybox could be a script, make sure /system/bin/sh exists + if [ ! -f /system/bin/sh ]; then + umount -l /system 2>/dev/null + mkdir -p /system/bin + ln -s $(command -v sh) /system/bin/sh + fi + + export ASH_STANDALONE=1 + + # Find our current arguments + # Run in busybox environment to ensure consistent results + # /proc//cmdline shall be