python-for-android icon indicating copy to clipboard operation
python-for-android copied to clipboard

ctypes.pythonapi issues; getting AttributeError: undefined symbol

Open SomberNight opened this issue 6 years ago • 12 comments

Using the new python3 toolchain, I am trying to use PyCryptodome (3.7+) as a dependency.

As in https://github.com/kivy/python-for-android/pull/1685, PyCryptodome >= 3.6.4 runs into compilation issues; I am trying to fix these too, though this is not that relevant here.

PyCryptodome >=3.6.0 crashes at runtime (since commit https://github.com/Legrandin/pycryptodome/commit/f5aa2c1618e97b6d773172fdd07794a0a6f05905). So actually the currently pinned version in the recipe does not work: https://github.com/kivy/python-for-android/blob/80e4f059c1ee0da48a7c85167087dfe5928ac395/pythonforandroid/recipes/pycryptodome/init.py#L5

The issue at runtime is with ctypes.

Say I have a main script that just does the following (https://github.com/Legrandin/pycryptodome/blob/95ccce7ae82d3a36f1a8652dd2c645222d0128dd/lib/Crypto/Util/_raw_api.py#L200):

import ctypes
ctypes.pythonapi.PyObject_GetBuffer

This works with cpython on my laptop, but with the p4a-compiled python on Android it fails:

06-14 19:06:27.053 15246 15274 I python  : Android kivy bootstrap done. __name__ is __main__
06-14 19:06:27.053 15246 15274 I python  : AND: Ran string
06-14 19:06:27.053 15246 15274 I python  : Run user program, change dir and execute entrypoint
06-14 19:06:27.092 15246 15274 I python  : Traceback (most recent call last):
06-14 19:06:27.092 15246 15274 I python  :   File "/home/user/wspace/electrum/.buildozer/android/app/main.py", line 84, in <module>
06-14 19:06:27.093 15246 15274 I python  :   File "/home/user/wspace/electrum/.buildozer/android/platform/build/build/other_builds/python3-libffi-openssl-sqlite3/armeabi-v7a__ndk_target_21/python3/Lib/ctypes/__init__.py", line 369, in __getattr__
06-14 19:06:27.093 15246 15274 I python  :   File "/home/user/wspace/electrum/.buildozer/android/platform/build/build/other_builds/python3-libffi-openssl-sqlite3/armeabi-v7a__ndk_target_21/python3/Lib/ctypes/__init__.py", line 374, in __getitem__
06-14 19:06:27.094 15246 15274 I python  : AttributeError: undefined symbol: PyObject_GetBuffer

I have also tried to access some other attributes of ctypes.pythonapi, such as Py_IncRef, which raises the same exception.

Any idea what's going on here? Why is ctypes.pythonapi non-functional?

SomberNight avatar Jun 14 '19 17:06 SomberNight

Not sure what's going on here, but it isn't impossible that this symbol is missing for some environment-related reason. I'll try to look into it.

Presumably this has worked for some people though, @AndreMiras any thoughts?

inclement avatar Jun 17 '19 19:06 inclement

I wanted to chime in and say I ran into the same problem, with a nearly identical stack trace. Any help is appreciated!

kevinl95 avatar Aug 23 '19 02:08 kevinl95

I'm currently using pycryptodome==3.4.6 for two of my projects without issue. I'll give 3.6.3 a try then

AndreMiras avatar Aug 23 '19 21:08 AndreMiras

I've tried playing with some pycryptodome primitives with the following code.

from typing import List

import Crypto
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes


def encrypt(key: bytes, data: bytes) -> List[bytes]:
    """
    Given a key and data returns a list containing nonce, tag and ciphertext.
    """
    cipher = AES.new(key, AES.MODE_EAX)
    ciphertext, tag = cipher.encrypt_and_digest(data)
    encrypted = [x for x in (cipher.nonce, tag, ciphertext)]
    return encrypted


def decrypt(key: bytes, data: bytes) -> bytes:
    """
    Given a key and data (nonce, tag and ciphertext), returns decrypted data.
    """
    nonce, tag, ciphertext = data
    cipher = AES.new(key, AES.MODE_EAX, nonce)
    decrypted = cipher.decrypt_and_verify(ciphertext, tag)
    return decrypted


def main():
    print(f'Crypto.version_info: {Crypto.version_info}')
    key = get_random_bytes(16)
    data = b'Testing pycryptodome on Python for Android'
    print(f'data: {data}')
    encrypted = encrypt(key, data)
    print(f'encrypted: {encrypted}')
    decrypted = decrypt(key, encrypted)
    print(f'decrypted: {decrypted}')
    assert data == decrypted


if __name__ == '__main__':
    main()

And it produced the following output on device.

08-24 00:28:10.660 14861 15109 I python  : Android kivy bootstrap done. __name__ is __main__
08-24 00:28:10.660 14861 15109 I python  : AND: Ran string
08-24 00:28:10.660 14861 15109 I python  : Run user program, change dir and execute entrypoint
08-24 00:28:13.172 14861 15109 I python  : Crypto.version_info: (3, 6, 3)
08-24 00:28:13.173 14861 15109 I python  : data: b'Testing pycryptodome on Python for Android'
08-24 00:28:13.190 14861 15109 I python  : encrypted: [b'\x7fJ\xa9P\x00-\x96G\x8d\x1d4=M3B\xc0', b'\xa5K\xc5@\xb1\xed\x88@\xa2\xf15\xb0\x81\xe4Sp', b'2S\xbfo\x85\x04\x8c\x15\x92h\x83\xaeQ\xeem\x93\xe9 |\x11Z|\x82=\n~\x99\xbd\xe6\xca\x0b\xbd<|\x81`\x94!I$\xdaX']
08-24 00:28:13.195 14861 15109 I python  : decrypted: b'Testing pycryptodome on Python for Android'
08-24 00:28:13.195 14861 15109 I python  : Python for android ended.

So I guess next up is to mess up with ctypes directly

AndreMiras avatar Aug 23 '19 22:08 AndreMiras

And I could confirm the issue with ctypes.

08-24 00:52:08.844 16982 17327 I python  : Run user program, change dir and execute entrypoint
08-24 00:52:08.866 16982 17327 I python  : ctypes.__version__: 1.1.0
08-24 00:52:08.909 16982 17327 I python  : Traceback (most recent call last):
08-24 00:52:08.909 16982 17327 I python  :   File "/home/andre/workspace/python-for-android/issues/1866/.buildozer/android/app/main.py", line 10, in <module>
08-24 00:52:08.911 16982 17327 I python  :   File "/home/andre/workspace/python-for-android/issues/1866/.buildozer/android/app/main.py", line 6, in main
08-24 00:52:08.912 16982 17327 I python  :   File "/home/andre/workspace/python-for-android/issues/1866/.buildozer/android/platform/build/build/other_builds/python3-libffi-openssl-sqlite3/armeabi-v7a__ndk_target_21/python3/Lib/ctypes/__init__.py", line 369, in __getattr__
08-24 00:52:08.913 16982 17327 I python  :   File "/home/andre/workspace/python-for-android/issues/1866/.buildozer/android/platform/build/build/other_builds/python3-libffi-openssl-sqlite3/armeabi-v7a__ndk_target_21/python3/Lib/ctypes/__init__.py", line 374, in __getitem__
08-24 00:52:08.914 16982 17327 I python  : AttributeError: undefined symbol: PyObject_GetBuffer
08-24 00:52:08.914 16982 17327 I python  : Python for android ended.

Code was:

import ctypes


def main():
    print(f'ctypes.__version__: {ctypes.__version__}')
    print(f'ctypes.pythonapi.PyObject_GetBuffer: {ctypes.pythonapi.PyObject_GetBuffer}')


if __name__ == '__main__':
    main()

Edit: This is in the C-API https://docs.python.org/3/c-api/buffer.html#c.PyObject_GetBuffer https://github.com/python/cpython/blob/3.7/Objects/abstract.c#L353 hence should be part of libpython3.7.so. And confirmed on Ubuntu 18.04:

readelf -s /usr/lib/python3.7/config-3.7m-x86_64-linux-gnu/libpython3.7.so | grep PyObject_GetBuffer
  1857: 000000000027b960    68 FUNC    GLOBAL DEFAULT   12 PyObject_GetBuffer

And in the p4a build.

readelf -s .buildozer/android/platform/build/dists/myapp/build/intermediates/jniLibs/debug/armeabi-v7a/libpython3.7m.so | grep PyObject_GetBuffer
   315: 0003b8e4    84 FUNC    GLOBAL DEFAULT   11 PyObject_GetBuffer

So it's as if we were not linking to it.

AndreMiras avatar Aug 23 '19 22:08 AndreMiras

Initially, in pycryptodome, ctypes is used as fallback to cffi, because of the "ImportError: CFFI with optimize=2 fails due to pycparser bug."

Is there any way to make cffi work in python-for-android, so that pycryptodome can work despite ctypes issues? I've seen in the current master some cmd line params like "no_compile_pyo" and "optimize_python", but I don't really get how/when they are used in the compilation toolchain (the pycryptodome files I find in the build directory are "pyc" and not "pyo", but they seem to be stripped from their docstrings anyway).

On the other hand, the fallback from cffi to ctypes, above, looks at sys.flags.optimize only, not the real state of some files... is there any way to make p4a run the python interpreter in non optimized mode, to see if it solves problems?

EDIT: I forced both flags in p4a to skip bytecode optimization, but files compiled into "_python_bundle/" are still without docstring, and anyway the python interpreter seems to be forced to use PYTHONOPTIMIZED=2 in multiple parts of the toolchain... so cffi seems unusable.

pakal avatar Dec 07 '19 23:12 pakal

This is not related to the main ctypes.pythonapi issue (which has maybe existed for very long), but if anyone needs to compile the latest pycryptodome for android, commenting out a few "Setenv("PYTHONOPTIMIZE", "2")" in start.c and in PythonActivity.java files allows it to use cffi and not hit that ctypes bug.

pakal avatar Dec 09 '19 22:12 pakal

For what it's worth, I've just noticed that other cross-compilation attempts to android did precisely patch "pythonapi" in ctypes, maybe it's what's missing in P4A?

The post: http://mdqinc.com/blog/2011/09/cross-compiling-python-for-android/

The patch: https://bitbucket.org/gabomdq/ignifuga/raw/tip/tools/patches/python.android.diff

pakal avatar Jan 24 '20 09:01 pakal

I'm facing the exact same issue. In which directory can I find the start.c that you've modified? Also what do I have to do exactly to fix the bug?

Also @AndreMiras does downgrading pycryptodome help or does downgrading ctypes help (if that is possible)

P.S. I am using pycryptodomex, but I don't think that should make a difference since the only difference I am aware of is the namespace.

ShreeSinghi avatar May 19 '20 10:05 ShreeSinghi

@ShreeSinghi my workaround works by searching the line Setenv("PYTHONOPTIMIZE", "2") in the source tree of P4A (there are multiple occurrences) and removing these lines ; this allows non-optimized mode for python compiled files, and thus makes ctypes use a different backend, without hitting the bug.

pakal avatar May 22 '20 18:05 pakal

Some other project's patch of python to support ctypes on Android is now here : https://github.com/gabomdq/ignifuga/blob/master/tools/patches/python.android.diff

Copy here in case it disappears again python.android.diff.zip

pakal avatar Sep 25 '21 09:09 pakal

For what it's worth : I've monkey-patched ctypes from the beginning of my main app, to force the loading of the proper python DLL, and it seems to work fine (PyCryptodome 3.9.9 works with this); thus no need to rely on CFFI by disabling PYTHONOPTIMIZE in P4A.

import ctypes, sys
ctypes.pythonapi = ctypes.PyDLL("libpython%d.%d.so" % sys.version_info[:2])   # replaces ctypes.PyDLL(None)

What would be the proper way to integrate this into P4A though?

With this I could update the PyCryptodome recipe.

pakal avatar Sep 25 '21 17:09 pakal