23

私はウェブアプリケーションを持っています。この一環として、アプリのユーザーがデータに対して実行する非常に単純なスクリプトを作成(またはコピーアンドペースト)できるようにする必要があります。

スクリプトは実際には非常に単純である可能性があり、パフォーマンスは最も小さな問題にすぎません。そして、私が意味するスクリプトの洗練された例は、次のようなものです。

ratio = 1.2345678
minimum = 10

def convert(money)
    return money * ratio
end

if price < minimum
    cost = convert(minimum)
else
    cost = convert(price)
end

ここで、価格とコストはグローバル変数です(環境にフィードして計算後にアクセスできるもの)。

しかし、私はいくつかのものを保証する必要があります。

  1. 実行されたスクリプトは、Pythonの環境にアクセスできません。ものをインポートしたり、明示的に公開していないメソッドを呼び出したり、ファイルの読み取りや書き込みを行ったり、スレッドを生成したりすることはできません。完全なロックダウンが必要です。

  2. スクリプトが実行される「サイクル」の数に厳しい制限を設けることができる必要があります。ここでは、サイクルは一般的な用語です。言語がバイトコンパイルされている場合は、VM命令である可能性があります。適用-評価/適用ループを呼び出します。または、スクリプトを実行する中央処理ループを反復処理するだけです。詳細は、しばらくして何かの実行を停止し、所有者に電子メールを送信して、「スクリプトは、いくつかの数字を足し合わせる以上のことをしているようです。それらを整理する」という私の能力ほど重要ではありません。

  3. Vanillaのパッチが適用されていないCPythonで実行する必要があります。

これまで私はこのタスクのために自分のDSLを書いてきました。私はそれを行うことができます。しかし、私は巨人の肩の上に構築できるかどうか疑問に思いました。これを行うPythonで利用可能なミニ言語はありますか?

ハッキーなLispバリアントはたくさんありますが(Githubで書いたものでも)、より専門的でない構文(たとえば、CまたはPascal)を使用したものを好みます。これは、コーディングの代替手段として検討しているためです。私自身もう少し成熟したものが欲しいです。

何か案は?

4

8 に答える 8

18

これがこの問題に対する私の見解です。ユーザースクリプトをバニラCPython内で実行する必要があるということは、ミニ言語用のインタープリターを作成するか、Pythonバイトコードにコンパイルして(またはPythonをソース言語として使用して)実行する前にバイトコードを「サニタイズ」する必要があることを意味します。

ユーザーがPythonでスクリプトを記述でき、解析ツリーから安全でない構文をフィルタリングしたり、安全でないオペコードを削除したりすることで、ソースとバイトコードを十分にサニタイズできるという前提に基づいて、簡単な例を示しました。バイトコード。

ソリューションの2番目の部分では、ユーザースクリプトのバイトコードがウォッチドッグタスクによって定期的に中断される必要があります。これにより、ユーザースクリプトがオペコードの制限を超えないようにし、これらすべてをバニラCPythonで実行します。

私の試みの要約。これは主に問題の2番目の部分に焦点を当てています。

  • ユーザースクリプトはPythonで書かれています。
  • バイトプレイを使用して、バイトコードをフィルタリングおよび変更します。
  • ユーザーのバイトコードをインストルメント化してオペコードカウンターを挿入し、コンテキストがウォッチドッグタスクに切り替わる関数を呼び出します。
  • グリーンレットを使用してユーザーのバイトコードを実行し、ユーザーのスクリプトとウォッチドッグコルーチンを切り替えます。
  • ウォッチドッグは、エラーを発生させる前に実行できるオペコードの数に事前設定された制限を適用します。

うまくいけば、これは少なくとも正しい方向に進むでしょう。あなたがそれに到達したとき、私はあなたの解決策についてもっと聞きたいです。

のソースコードlowperf.py

# std
import ast
import dis
import sys
from pprint import pprint

# vendor
import byteplay
import greenlet

# bytecode snippet to increment our global opcode counter
INCREMENT = [
    (byteplay.LOAD_GLOBAL, '__op_counter'),
    (byteplay.LOAD_CONST, 1),
    (byteplay.INPLACE_ADD, None),
    (byteplay.STORE_GLOBAL, '__op_counter')
    ]

# bytecode snippet to perform a yield to our watchdog tasklet.
YIELD = [
    (byteplay.LOAD_GLOBAL, '__yield'),
    (byteplay.LOAD_GLOBAL, '__op_counter'),
    (byteplay.CALL_FUNCTION, 1),
    (byteplay.POP_TOP, None)
    ]

def instrument(orig):
    """
    Instrument bytecode.  We place a call to our yield function before
    jumps and returns.  You could choose alternate places depending on 
    your use case.
    """
    line_count = 0
    res = []
    for op, arg in orig.code:
        line_count += 1

        # NOTE: you could put an advanced bytecode filter here.

        # whenever a code block is loaded we must instrument it
        if op == byteplay.LOAD_CONST and isinstance(arg, byteplay.Code):
            code = instrument(arg)
            res.append((op, code))
            continue

        # 'setlineno' opcode is a safe place to increment our global 
        # opcode counter.
        if op == byteplay.SetLineno:
            res += INCREMENT
            line_count += 1

        # append the opcode and its argument
        res.append((op, arg))

        # if we're at a jump or return, or we've processed 10 lines of
        # source code, insert a call to our yield function.  you could 
        # choose other places to yield more appropriate for your app.
        if op in (byteplay.JUMP_ABSOLUTE, byteplay.RETURN_VALUE) \
                or line_count > 10:
            res += YIELD
            line_count = 0

    # finally, build and return new code object
    return byteplay.Code(res, orig.freevars, orig.args, orig.varargs,
        orig.varkwargs, orig.newlocals, orig.name, orig.filename,
        orig.firstlineno, orig.docstring)

def transform(path):
    """
    Transform the Python source into a form safe to execute and return
    the bytecode.
    """
    # NOTE: you could call ast.parse(data, path) here to get an
    # abstract syntax tree, then filter that tree down before compiling
    # it into bytecode.  i've skipped that step as it is pretty verbose.
    data = open(path, 'rb').read()
    suite = compile(data, path, 'exec')
    orig = byteplay.Code.from_code(suite)
    return instrument(orig)

def execute(path, limit = 40):
    """
    This transforms the user's source code into bytecode, instrumenting
    it, then kicks off the watchdog and user script tasklets.
    """
    code = transform(path)
    target = greenlet.greenlet(run_task)

    def watcher_task(op_count):
        """
        Task which is yielded to by the user script, making sure it doesn't
        use too many resources.
        """
        while 1:
            if op_count > limit:
                raise RuntimeError("script used too many resources")
            op_count = target.switch()

    watcher = greenlet.greenlet(watcher_task)
    target.switch(code, watcher.switch)

def run_task(code, yield_func):
    "This is the greenlet task which runs our user's script."
    globals_ = {'__yield': yield_func, '__op_counter': 0}
    eval(code.to_code(), globals_, globals_)

execute(sys.argv[1])

これがサンプルのユーザースクリプトuser.pyです:

def otherfunc(b):
    return b * 7

def myfunc(a):
    for i in range(0, 20):
        print i, otherfunc(i + a + 3)

myfunc(2)

実行例は次のとおりです。

% python lowperf.py user.py

0 35
1 42
2 49
3 56
4 63
5 70
6 77
7 84
8 91
9 98
10 105
11 112
Traceback (most recent call last):
  File "lowperf.py", line 114, in <module>
    execute(sys.argv[1])
  File "lowperf.py", line 105, in execute
    target.switch(code, watcher.switch)
  File "lowperf.py", line 101, in watcher_task
    raise RuntimeError("script used too many resources")
RuntimeError: script used too many resources
于 2011-03-04T06:57:52.513 に答える
8

ジスピーはぴったりです!

  • これはPythonのJavaScriptインタープリターであり、主にPythonにJSを埋め込むために構築されています。

  • 特に、再帰とループのチェックと上限を提供します。必要に応じて。

  • これにより、Python関数をJavaScriptコードで簡単に利用できるようになります。

  • デフォルトでは、ホストのファイルシステムやその他の機密要素は公開されません。

完全開示:

  • Jispyは私のプロジェクトです。私は明らかにそれに偏っています。
  • それにもかかわらず、ここでは、それは本当に完璧にフィットしているように見えます。

PS:

  • この回答は、この質問が行われてから約3年後に書かれています。
  • そのような遅い答えの背後にある動機は単純です:
    Jispyが目前の質問にどれほど密接に限定するかを考えると、同様の要件を持つ将来の読者はそれから利益を得ることができるはずです。
于 2014-11-17T18:49:16.037 に答える
5

Luaを試してください。あなたが言及した構文は、Luaのものとほとんど同じです。LuaをPython3.xに埋め込むにはどうすればよいですか?を参照してください。

于 2011-02-24T03:34:00.760 に答える
4

この問題を本当に解決するものはまだ何も知りません。

私ができる最も簡単なことは、Pythonで独自のバージョンのPython仮想マシンを作成することだと思います。

Cythonのようなものでそれを行うことをよく考えていたので、モジュールとしてインポートするだけで、ほとんどのハードビットについて既存のランタイムに頼ることができました。

すでにPyPyを使用してpython-in-pythonインタープリターを生成できる場合がありますが、PyPyの出力は、組み込み型などの基盤となるPyObjectsと同等のものを実装するなど、すべてを実行するランタイムです。このようなこと。

本当に必要なのは、実行スタックのフレームのように機能するものと、各オペコードのメソッドです。自分で実装する必要すらないと思います。既存のフレームオブジェクトをランタイムに公開するモジュールを作成するだけで済みます。

とにかく、フレームオブジェクトの独自のスタックを維持し、バイトコードを処理するだけで、1秒あたりのバイトコードなどでスロットルできます。

于 2011-02-24T04:00:59.113 に答える
2

以前のプロジェクトでは、Pythonを「ミニ構成言語」として使用しました。私のアプローチは、コードを取得し、parserモジュールを使用して解析してから、生成されたコードのASTをウォークし、「許可されていない」操作(たとえば、クラスの定義、__メソッドの呼び出しなど)を開始することでした。

これを行った後、「許可」されたモジュールと変数のみを使用して合成環境を作成し、その中のコードを評価して、実行できるものを取得しました。

それは私にとってうまくいきました。特に、構成言語の場合よりも強力なパワーをユーザーに提供したい場合は、それが防弾であるかどうかはわかりません。

制限時間については、プログラムを別のスレッドまたはプロセスで実行し、一定時間後に終了することができます。

于 2011-02-24T03:45:53.077 に答える
1

pysandbox http://pypi.python.org/pypi/pysandbox/1.0.3のPythonコードを使用してみませんか?

于 2011-02-24T00:37:27.463 に答える
1

LimPyを見てください。これはLimitedPythonの略で、まさにこの目的のために構築されました。

ユーザーエクスペリエンスを制御するための基本的なロジックをユーザーが作成する必要がある環境がありました。実行時の制限とどのように相互作用するかはわかりませんが、少しのコードを記述したいのであれば、それができると思います。

于 2011-02-24T01:01:01.377 に答える
-1

実際のDSLを作成する最も簡単な方法はANTLRで、いくつかの一般的な言語の構文テンプレートがあります。

于 2011-02-24T00:42:02.260 に答える