-
Notifications
You must be signed in to change notification settings - Fork 519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Buildozer silently ignores local recipes – and this is bad #1433
Comments
Thank you for the really detailed report! Will try to give it a deeper look ASAP. |
Excellent. Thanks so much for sinking your incisors into this! I'm both delighted and appalled everything works on your end, though. Deterministic issues are so much simpler to resolve. Could there possibly be a caching issue here where Buildozer previously cached a correctly resolved relative Relatedly, I just noticed the existing |
Stranger in a strange land... is exactly how I feel. Because you're sorta right, @misl6; after exhaustively injecting However, I can unhappily confirm that Buildozer is still silently ignoring all of the local recipes in that directory. That's bad. This issue is thus still a thing. I am left scratching my head, which is now openly suppurating with oozing wounds. Mind-breaking puzzle: you continue to haunt me. 👻 |
ZOMG. Buildozer is silently ignoring all of the local recipes in
Instead, Buildozer should be appending recipes for Let's debug up the call chain and see why the |
Ugh. Looks like the eventual culprit is the def dist_from_args(ctx, args):
"""Parses out any distribution-related arguments, and uses them to
obtain a Distribution class instance for the build.
"""
return Distribution.get_distribution(
ctx,
name=args.dist_name,
recipes=split_argument_list(args.requirements), # <-- THIS SO BROKY
archs=args.arch,
ndk_api=args.ndk_api,
force_build=args.force_build,
require_perfect_match=args.require_perfect_match,
allow_replace_dist=args.allow_replace_dist) Ugh x 2. Looks like the real eventual culprit is that |
ZOMGZOMGZOMG. Much to my complete dismay and astonishment, I believe I finally uncovered the ultimate culprit: it's the Instead, that constructor appears to derive its list of initial recipes from two divergent sources:
In our case, our proprietary project has a standard THIS IS ALL WRONG, BROThis is all wrong, @misl6. I 😍 everything else you've done with the Kivy ecosystem, but this is... not right. The
It's not just that the Even
That's how hard packaging in Python is. Python packaging is hollowed ground. You don't trespass on hollowed ground without good reason. Buildozer doesn't have that reason, because Buildozer already has the Let Me Help YouI package for Gentoo Linux. I package for Let's Stop the Parsing MadnessBuildozer should never attempt to parse I note the use of an upstream If you'd really like to continue using package dependencies (which you shouldn't), Buildozer should instead defer to the core The only snag there is that In short: Let's Start Using
|
ZOMGZOMGZOMG. I can't believe it, but the real, real issue here is that Buildozer silently ignores requirements containing the
...then Buildozer suddenly begins accepting and using those requirements as recipes. So, let's just ignore everything above about This Is Really Not RightDuring app development and testing, I definitely do not want to pin exact dependency versions. During app release and version bumps? Sure. I suppose, but probably not even then. We intend our app to portably work with a reasonable range of dependency versions, because the alternative is unmaintainable madness. It helps that our dependencies avoid breaking backward compatibility – or at least explicitly warn us with obvious deprecations when they intend to do so in a future release. The only time I've ever really wanted to pin exact dependencies was in the web dev space, because web dev is fundamentally broken by design. That's the nature of the beast. So you limit the breakage as best you can with pinning, semantic versioning (SemVer), and the This Can Be Made RightIn short, I'd like to:
Does this erratic gameplan seem reasonable, @misl6? If so, I'll submit a series of well-tested PRs – first addressing 2. (because the user should always be informed when Buildozer disagrees with something) and then addressing 1.. Let's do this, Team Kivy! ✊ |
Fascinating... and horrifying! Buildozer is correctly sending the That simplifies matters a lot. Good news descends from the heavens like mana. We'll only need to submit PRs against |
Oh... oh by Gods. Could it be? N-n-no. It couldn't possibly... But it could.
OH MY LIVING GODS BELOW. That's exactly what Buildozer is doing: $ ls .buildozer/android/platform/python-for-android/pythonforandroid
. pythonforandroid '=0.14.0,kivy' '=3.9.0,bleak' Dockerfile Makefile
.. testapps '=0.14.0,numpy' '=3.9.0,kivy' .dockerignore MANIFEST.in
ci tests '=1.22.0,scipy' CHANGELOG.md .env .projectile
doc '=0.10.0,bleak' '=1.7.0,sympy' .coveragerc .gitignore README.md
.git '=0.10.0,numpy' '=1.9.0' .deepsource.toml ionyou-debug-0.0.1-.apk setup.py
.github '=0.104.2,beartype' '=2.1.0,kivymd' distribute.sh LICENSE tox.ini I'm sitting stunned here – mouth agape like Soyboy Wojak. I cannot believe my cheating eyes. How is this possible, @misl6? How is it possible that a mission-critical cross-compiling toolchain with over 300 financial backers and 20 corporate supporters in 2022 is naively running shell commands without quote-protecting shell arguments? The Sky Is Officially FallingThis is a serious security risk, bro. No. Seriously. Full-stop. This is injection attack territory. Buildozer is executing arbitrary strings in
Buildozer is now in imminent danger of having a CVE posted against it. The severity of this issue cannot be overstated. If I'm reading the codebase right (...I'm probably not), the def compile_platform(self):
...
p4a_create = "create --dist_name={} --bootstrap={} --requirements={} ".format(dist_name, self._p4a_bootstrap, requirements) 🤦 🤦♂️ 🤦♀️ Never interpolate raw arbitrary strings directly into shell commands. Please. Gods both above and below. Please stop doing that before the Internet fully implodes into itself. Strings deriving from external sources (including
from shlex import quote
...
def compile_platform(self):
...
p4a_create = "create --dist_name={} --bootstrap={} --requirements={} ".format(
quote(dist_name),
quote(self._p4a_bootstrap),
quote(requirements),
) The road to Hell is paved with manual shell command interpolation. tl;drBuildozer is fundamentally insecure against shell injection attacks. Resolving CVEs is way outside my wheelhouse as a volunteer minion, so I'm just going to quietly show myself the door while devoutly praying to every northern Canadian mosquito swamp deity I know that someone who is not me fixes everything before the corporate donors justifiably storm the gates. |
First, thanks a lot for the investigation. Indeed security was certainly not a major concern during the design of buildozer, as it’s usually ran manually by the user wishing to build their application, while that’s certainly no excuse, as seemingly unexploitable bugs can always find their use in chains later as people rely on software and building more complex solutions on it without understanding their security characteristics. Also, it’s a stupid and dangerous bug no matter how you see it, and it should be solved, no argument. I don’t remember from the top of my head why we pass This should get priority in solving. |
Even if I'm not too concerned about the security issue found during the discussion, I'm taking this occasion to refresh some code. The weekend plan was to work on a Why I'm not concerned:There are plenty of ways (even simpler ones than the one discussed here) of injecting malicious shell commands into the developer shell (which I'm not going to discuss here, for obvious reasons) when using Why even if I'm not concerned I'm working on it:
Agree, and that's an excuse to refresh some code. What we should do in the future:Set up a Security Policy so we can talk about this kind of issues privately (and later share info with the community about security bugs when are already fixed (or at least triaged), and not exploitable anymore). [ Was on my low-priority task list ] |
tl;dr
Relative dirnames in
p4a.local_recipes
and--local_recipes
have been broken for a year or two. Until this issue is resolved, please replace the./
prefixing these dirnames with../../../../
instead: e.g.,Rejoice as local recipes suddenly work. If you too are now facepalming, this emoji's for you. 🤦
Versions or It Didn't Happen
1.3.0
Exegesis or It Didn't Happen
Buildozer silently ignores relative dirnames for both the
p4a.local_recipes
setting inbuildozer.spec
files and the--local_recipes
option accepted by mostp4a
subcommands. Since the only sane means of specifying a local recipe directory is with a relative dirname, local recipes have effectively been broken for at least a year or two... which is bad.Clearly, local recipes used to work. As this issue will show, they no longer do. The breakage probably happened when Buildozer was refactored to change the current working directory (CWD) to the deeply nested
.buildozer/android/platform/python-for-android/
subdirectory before runningp4a
. Previously, Buildozer must not have done that; it must have preserved the CWD as the current top-level project directory containing the rootbuildozer.spec
file. Now, Buildozer changes the CWD to a deeply nested subdirectory breaking relative dirnames inbuildozer.spec
files.Let's dissect this. ✂️
buildozer.spec
Let's use this spec file to build the
Blap
app, Buildozer!Let's define a trivial recipe for the
bleak
requirement that mostly does nothing except exist!Lastly, let's build the Android
Blap
app, Buildozer!We are now ready to break you, Buildozer.
Logs
There's little point in pasting the full output, so let's reduce this to the only line that matters:
$ buildozer android debug ... [INFO]: blap: min API 21, includes recipes (hostpython3, libffi, openssl, sdl2_image, sdl2_mixer, sdl2_ttf, sqlite3, python3, sdl2, setuptools, six, pyjnius, android), built for archs (arm64-v8a, armeabi-v7a)
Note the conspicuous absence of
bleak
above. So, Buildozer has failed to find our localp4a-recipes/bleak
recipe. B-b-but whatever could the matter be...? Just kidding! I figured everything out already. It's thepythonforandroid.recipe.Recipe.recipe_dirs()
classmethod, which is painfully broken with respect to Buildozer's CWD behaviour.</sigh>
It's Raining Solutions
Ah-ha. Bet you weren't expecting that one, were you, @misl6? Nobody ever submits solutions. They only complain endlessly. Right? I know. I'm usually one of those people, because I am lazy. But complaining mostly doesn't work, as evidenced by the 199 currently open issues on this tracker.
So... I did a deep dive on this for the community. There are multiple intersecting issues here, each of which should be resolved before this issue is eventually closed. They all involve the aforementioned
pythonforandroid.recipe.Recipe.recipe_dirs()
classmethod, whose implementation is so naive it makes me just want to stop doing anything and play Persona 5 Royal for the 10th time. Specifically, that classmethod:p4a.local_recipes
and--local_recipes
are both utterly broken – and have been that way for a few years now. But tests pass. Ergo, someone who is not me really needs to write working unit and integration tests ensuring this never happens again. Of course, test coverage is at a shocking low of 39%, so... what you gonna do? It doesn't help that both Buildozer andpython-for-android
need additional tests:python-for-android
is actually respecting the Buildozer-specificp4a.local_recipes
setting. It currently isn't.python-for-android
needs unit tests ensuring that theRecipe.recipe_dirs()
classmethod:p4a.local_recipes
or--local_recipes
) that does not exist, this classmethod needs to raise a human-readable exception. Currently, this classmethod silently ignores this edge case – which is actually all cases, because this classmethod erroneously canonicalizes most relative dirnames to non-existent absolute dirnames. Cue more facepalm emojis. 🤦♂️ 🤦♀️p4a.local_recipes = ./p4a-recipes/
or--local_recipes=./p4a-recipes/
, they expect the./
prefixing that dirname to refer to the top-level project directory containing the rootbuildozer.spec
file. Instead, Buildozer unexpectedly:python-for-android
subdirectory:python-for-android
subdirectory rather than against the top-level project directory. Why does this classmethod do that? Because this classmethod is naive:See the line conspicuously marked
# <-- THIS IS BALLS
? Yeah. That's the problem. When that line is interpreted,ctx.local_recipes == './p4a-recipes/'
. Since Buildozer changes the CWD beforehand to the.buildozer/android/platform/python-for-android/
subdirectory, passing that relative dirname to theos.path.realpath()
function incorrectly expands that dirname to.buildozer/android/platform/python-for-android/p4a-recipes/
.Of course, that directory doesn't actually exist – but
recipe_dirs()
doesn't care.recipe_dirs()
is naive. It crosses its fingers and just silently appends that non-existent directory to the returned list of recipe dirnames. Cue badness. 🤞Here's what the
Recipe.recipe_dirs()
classmethod needs to look like instead:Crucially, we now:
Wunderbar. So, why haven't I already submitted a PR conveniently solving this for everyone? Because there is no
ctx.project_dir
. I just made that up, you see. Thepythonforandroid.build.Context
class doesn't seem to provide the project directory, which leaves that prospective PR with nowhere good to go.Internal documentation in the
python-for-android
codebase is so scarce that I can't be sure, though. The project directory must be accessibly stored somewhere, right? That's the most important directory. Everything else is just a footnote to the project directory. It'd be kinda sad (but ultimately unsurprising) if the build context fails to capture the most important build context. The feeling of vertigo is now overwhelming. 😵💫I now beseech someone with greater wisdom than me (...so, basically anyone) to do this for me.
Everything, It Now Makes Sense
There's actually a related open issue. Ironically, that issue was submitted by @dlech – the active maintainer of Bleak, whose recipe prompted both of these issues. Double-ironically, @dlech also identified the wacky short-term workaround for this insane long-standing issue: just replace the
./
substring prefixing ap4a.local_recipes
dirname with../../../../
. Since the.buildozer/android/platform/python-for-android
subdirectory is nested four levels deep, four levels of "de-nesting" are required to break out of the Buildozer-isolated p4a directory and return back to the top-level project directory. Note the judicious use of adjectives like "wacky" and "insane."There's also a fascinating Reddit thread on the
/r/kivy
subreddit debating this very topic. Numerous Reddit users self-report the same issue. Of course, no one knew how to resolve the issue or that there even was an issue. Instead, everyone uselessly flailed around and (presumably) eventually abandoned Kivy because nothing was working. I'll admit I would have done the same in their position.The fact that
p4a.local_recipes
and--local_recipes
broken also breaks Buildozer's official documentation on the subject as well as downstream projects previously leveraging these things.Sad cats unite. 😹 😿 🙀
The text was updated successfully, but these errors were encountered: