[BUG] "unauthorized use of script" when packing with pyinstaller and using private/restrict.
I have a project with two files:
a.py:
def p(x):
print(f"Hello {x}")
main.py:
import a
a.p("world")
My goal is to use pyinstaller to create a single binary I can distribute. I want to make sure user cannot decompile the binary and import package a.py. I build main.py and its a.py dependency into a binary:
pyinstaller -F main.py
I then try to --pack with the --private:
pyarmor gen -O obfdist --pack dist/main a.py main.py --private
But I get this error when running the built binary.
./dist/main
Traceback (most recent call last):
File "<frozen __main__>", line 3, in <module>
File "<frozen main>", line 1, in <module>
File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
File "PyInstaller/loader/pyimod02_importers.py", line 385, in exec_module
File "<frozen a>", line 3, in <module>
RuntimeError: unauthorized use of script (1:1380)
[21628] Failed to execute script 'main' due to unhandled exception!
I've also tried using both --restrict and restrict_module=2 (as stated in #1274), but I get the same error.
./dist/main
Traceback (most recent call last):
File "<frozen __main__>", line 3, in <module>
File "<frozen main>", line 1, in <module>
File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
File "PyInstaller/loader/pyimod02_importers.py", line 385, in exec_module
File "<frozen a>", line 3, in <module>
RuntimeError: unauthorized use of script (1:1380)
[21939] Failed to execute script 'main' due to unhandled exception!
Is there any way to protect the packages with pyarmor when using pyinstaller? Otherwise, a user could decompile the binary and import my packages.
PyInstaller loader is different from Python Interpreter, so the script executed by PyInstaller loader must be no restrict.
Here is an example to show how to use --private with pack
Suppose the main script is foo.py
import fib
def main():
print('Hello', fib.message)
if __name__ == '__main__':
main()
The content of fib.py
message = 'Fibo'
Then it need create fake main script fake_main.py
from foo import main
main()
Now generate executable dist/foo/foo
pyinstaller --name foo fake_main.py
And pack the obfuscated script by --private but exclude fake_main.py
pyarmor cfg exclude_restrict_modules="fake_main"
pyarmor gen --private --pack dist/foo/foo fake_main.py foo.py fib.py
Test it
dist/foo/foo
It has been tested in Linux and with PyInstaller 5.13.2
Thanks for directing me here and suggesting a workaround. It appears that exclude_restrict_modules is unprivatizing other modules as well due to the bug reported below
https://github.com/dashingsoft/pyarmor/issues/1585
If you replace the filenames fake_main, foo, fib with the filenames a, b, c, it appears to reproduce the problem.
@jondy
Thank you for the 8.4.7 release. Unfortunately, in 8.4.7, the problem seems to reproduce itself even if we modify the code as suggested.
(py38) C:\Works\pyarmor>dist\foo\foo.exe
Hello Fibo
(py38) C:\Works\pyarmor>pyarmor cfg exclude_restrict_modules="fake_main"
INFO Python 3.8.5
INFO Pyarmor 8.4.7 (trial), 000000, non-profits
INFO Platform windows.x86_64
INFO change option "exclude_restrict_modules" to new value "fake_main"
------------------------------------------------------------
Section: builder
Current settings
exclude_restrict_modules = __init__
Global settings
Local settings
exclude_restrict_modules = fake_main
(py38) C:\Works\pyarmor>pyarmor gen --private --pack dist/foo/foo.exe fake_main.py foo.py fib.py
INFO Python 3.8.5
INFO Pyarmor 8.4.7 (trial), 000000, non-profits
INFO Platform windows.x86_64
INFO implicitly set output to ".pyarmor\pack\dist"
INFO extracting bundle "dist/foo/foo.exe"
INFO search inputs ...
INFO find script fake_main.py
INFO find script foo.py
INFO find script fib.py
INFO find 3 top resources
INFO start to generate runtime files
INFO target platforms {'windows.amd64'}
INFO write .pyarmor\pack\dist\pyarmor_runtime_000000\pyarmor_runtime.pyd
INFO start to obfuscate scripts
INFO process resource "fake_main"
INFO obfuscating file fake_main.py
INFO write .pyarmor\pack\dist\fake_main.py
INFO process resource "foo"
INFO obfuscating file foo.py
INFO write .pyarmor\pack\dist\foo.py
INFO process resource "fib"
INFO obfuscating file fib.py
INFO write .pyarmor\pack\dist\fib.py
INFO obfuscate scripts OK
INFO repacking bundle "dist/foo/foo.exe"
INFO obfuscated scripts at ".pyarmor\pack\dist"
INFO entry script name is "foo.py"
INFO repacking "PYZ-00.pyz"
INFO replace item "pyarmor_runtime_000000"
INFO replace item "fib"
INFO replace item "foo"
INFO repacking PKG "PKG-patched"
INFO replace entry "fake_main"
INFO replace entry "PYZ-00.pyz"
INFO repacking EXE "dist/foo/foo.exe"
INFO replace PKG with "PKG-patched"
INFO generate patched bundle "dist/foo/foo.exe" successfully
(py38) C:\Works\pyarmor>python fake_main.py
Hello Fibo
(py38) C:\Works\pyarmor>dist\foo\foo.exe
Traceback (most recent call last):
File "<frozen __main__>", line 3, in <module>
File "<frozen fake_main>", line 1, in <module>
File "<frozen importlib._bootstrap>", line 991, in _find_and_load
File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
File "PyInstaller\loader\pyimod02_importers.py", line 499, in exec_module
File "<frozen foo>", line 3, in <module>
RuntimeError: unauthorized use of script (1:1380)
[25556] Failed to execute script 'fake_main' due to unhandled exception!
(py38) C:\Works\pyarmor>
Got it, it seems fake_main with exclude_restrict_modules doesn't work.
I'll check it again
Here is one solution by patching PyInstaller\loader\pyimod02_importers.py
The idea is to make PyInstaller module loader like CPython interpreter
In the traceback, there is a line
File "PyInstaller\loader\pyimod02_importers.py", line 499, in exec_module
So edit /path/to/PyInstaller\loader\pyimod02_importers.py line 499 to
# exec(bytecode, module.__dict__)
def _call_with_frames_removed():
exec(bytecode, module.__dict__)
_call_with_frames_removed()
After that no fake_main and no exclude_restrict_modules, only pack with --private. For example,
pyarmor cfg -r exclude_restrict_modules
rm -rf dist
pyinstaller foo.py
pyarmor gen --private --pack dist/foo/foo foo.py fib.py
dist/foo/foo
Thanks for the suggestion. However, my concern is that if we change the loader, we may need to support the license. We will consider whether we can accommodate this.
No. 5 in the License section https://github.com/pyinstaller/pyinstaller/wiki/FAQ
Based on your suggestion, I think I can work around it for the time being by dynamically rewriting the module so that it does not affect the license.
Thanks for your help.
fake_main.py
import sys
if mod := sys.modules.get("pyimod02_importers"):
mod.exec = lambda *args, **kwargs: exec(*args, **kwargs)
from foo import main
main()
This is temporary solution, if this patch works, in next release v8.5.0, it will be implemented in the pyarmor side, so need do nothing in PyInstaller.
Fixed in v8.5.0