继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

自制代码解释器:沙箱搭建记

冉冉说
关注TA
已关注
手记 353
粉丝 43
获赞 194

我们中的许多人经常想知道ChatGPT是如何在其环境中安全地执行代码的。虽然我们不清楚OpenAI的具体做法,但我们尝试用类似的基本原则来创建一个类似系统。

在这一系列文章中,我们将构建一个具有以下功能的系统,

  1. 生成你的代码
  2. 安全运行这段代码
  3. 上传你的数据文件并下载结果

第一部分——根据提示自动生成代码——已经得到了像OpenAI这样的模型的良好支持。我们将在后续的文章中更详细地讨论这方面的内容。目前,我们将重点放在安全运行代码以及安全处理文件上传和下载方面。

我们为什么需要一个沙箱呢?

最简单的形式来说,沙箱是一个安全的、封闭的空间,孩子们可以安全地在里面玩耍而不会受伤。同样地,在计算机领域,沙箱允许我们在受控环境中运行可能不安全的代码,这样就避免了对系统造成损害。

Python 提供了一种基本的方式来执行代码,使用 exec() 函数,该函数可以执行作为字符串形式提供的 Python 代码,比如。

exec(generated_code, global_variables, local_variables)
# 执行生成的代码,使用全局变量和局部变量

这里,generated_code 是你想要运行的代码,而 global_variableslocal_variables 是包含代码可使用的变量的字典。这是一种简单的运行代码的方式,但如果没有适当的保护措施,这可能很危险。这也是我们在本系列中将探讨如何构建安全沙箱的原因。

这里是一个运行由大型语言模型生成代码的例子。

    import pandas as pd  
    import numpy as np  

    # 定义一个DataFrame,包含随机数据,并且把它放在exec语句块之外  
    df = pd.DataFrame(np.random.randn(5, 3), columns=["A", "B", "C"])  

    # 定义要执行的脚本  
    python_script = '''  
    import pandas as pd  
    # 一些用于演示的局部变量  
    local_var1 = 42  
    local_var2 = "Hello, exec()"  
    local_var3 = df["A"].mean()  
    '''  

    # 准备一个空的局部命名空间字典  
    local_namespace = {}  

    # 使用exec()执行脚本,并将df作为全局命名空间的一部分传递  
    exec(python_script, {"df":df}, local_namespace)  

    # 执行后捕获的局部变量:  
    print("\n执行后捕获的局部变量:")  
    for var, value in local_namespace.items():  
        if not var.startswith('__'):  # 忽略内置的变量  
            print(f"{var}: {value}")  
    ############## 输出 #############################  
    执行 exec() 后捕获的局部变量。  
    pd: <module 'pandas' from '/Users/symphonyai/anaconda3/envs/pd_agent/lib/python3.11/site-packages/pandas/__init__.py'>  
    np: <module 'numpy' from '/Users/symphonyai/anaconda3/envs/pd_agent/lib/python3.11/site-packages/numpy/__init__.py'>  
    local_var1: 42  
    local_var2: Hello, exec()  
    local_var3: -0.012990217992773223
自残程序

在编写和运行代码时,尤其是在生产环境中,安全是最重要的一点。如果没有适当的防护措施,即使是简单的代码生成指令也可能导致严重问题,如果系统被攻击或破坏。比如,如果有人注入一个旨在删除关键文件或关闭系统的指令,可能会导致严重的中断。

以下是一些可能有害的代码示例,例如:

    import os  
    os.system("sudo shutdown -h now")  # 关闭计算机

该脚本会关闭正在运行它的电脑。

    import os  
    import sys  
    import glob  
    # 获取当前脚本所在目录  
    current_directory = os.path.dirname(os.path.abspath(__file__))  
    # 获取目录下所有文件  
    files = glob.glob(os.path.join(current_directory, '*'))  
    # 删除每个文件:  
    for file_path in files:  
        try:  
            os.remove(file_path)  
            print(f"删除了: {file_path}")  
        except Exception as e:  
            print(f"删除 {file_path} 时出错: {e}")

这段脚本会删除当前文件夹中的所有文件,这会造成灾难性后果。另一个例子就是类似这样的代码,会删除一些重要的 Python 包(如 Python 自带的包),这可能会造成混淆。

    import importlib.util  
    import shutil  
    import os  
    def delete_package(package_name):  
        # 查找包的所在位置  
        spec = importlib.util.find_spec(package_name)  
        if spec is None:  
            print(f"未找到该 {package_name} 包。")  
            return  

    # 找到包的目录  
        package_path = spec.submodule_search_locations[0]  
        # 尝试移除包  
        try:  
            shutil.rmtree(package_path)  
            print(f"成功删除 {package_name} 包。")  
        except Exception as e:  
            print(f"删除 {package_name} 包失败: {e}")  
    # 例如,我们可以删除 pandas 包  
    delete_package('pandas')

运行这会删除如 pandas 这样的重要包,可能把你的整个代码库搞坏。

隔离执行的环境
隔离运行环境

为了避免这样的灾难,我们可以设立一个沙箱——一个独立隔离的执行环境——在那里可以安全运行代码,而不危及主系统的安全。

想法是用两个系统。

  1. 主系统:我们在这里与用户或代理进行交互并管理所有其他任务的执行。
  2. 沙盒系统:一个独立且隔离的环境(例如虚拟机、云实例或另一台计算机),生成的代码会在其中执行。该系统被设计为独立运行代码,并将结果返回给主系统而不对其造成影响。
交流

为了让这两个系统能够互相交流,我们将使用一个API调用。主系统会通过网址或端口将生成的代码及其所需的文件发送到沙盒系统。沙盒会运行这段代码,执行完毕后,它会把结果发回主系统。这与OpenAI API的工作方式相似。

创建一个沙盒 app

现在我们了解了安全沙箱的重要性,让我们着手创建一个。到本指南结束时,我们应该能够在Docker容器中运行一个沙箱应用程序,从而能够在不损害我们主系统的完整性的情况下安全地执行代码。本指南不仅关于构建沙箱应用程序,你也可以直接从Docker Hub拉取最终镜像,但不必从头开始构建,还关于理解FastAPI、Docker镜像和容器是如何协同工作的。这些知识对于数据科学家和工程师来说非常重要,因为现代工程系统越来越依赖于Docker容器和API技术。

第一步:创建文件夹结构

首先,我们来创建以下文件夹结构。我们先创建这些文件但暂时不填充内容;我们会一步步地完成它们。

    工作文件夹目录  
    ├── session.py  
    ├── test_session_and_main.py  
    └── 沙盒文件夹  
        ├── requirements.txt  
        ├── main.py  
        └── Dockerfile
步骤二:安装依赖项

将以下内容复制到 requirements.txt 文件中,然后在打开终端并切换到 sandbox 文件夹后,请运行以下命令。

    # 文件名 - requirements.txt  
    fastapi==0.112.0  
    pandas==2.2.2  
    uvicorn==0.30.6  
    numpy==1.23.2  # Corrected to a realistic version  
    python-multipart==0.0.9  
    requests==2.32.3  
    plotly==5.23.0

安装依赖项,请执行:

    pip install -r requirements.txt
第三步:创建 FastAPI 应用程序

接下来,把下面的代码拷贝到 main.py 文件里:

    # 文件名 - main.py
    from fastapi import FastAPI, HTTPException
    from fastapi import UploadFile, File
    from fastapi.responses import JSONResponse
    import pandas as pd
    from io import BytesIO
    import json
    import pickle

    # 实例化FastAPI应用
    app = FastAPI()

    # 初始化字典用于上传文件和保存结果
    df_dict = {}
    result_dict = {}

    @app.post("/uploadfile/{file_name}")
    async def upload_file(file_name: str, file: UploadFile = File(...)):
        """
        此函数管理文件的上传。
        将文件内容读取并保存到pandas数据框,然后存入df_dict字典中
        """
        # 读取文件内容
        content = await file.read()
        df = pd.read_csv(BytesIO(content))
        df_dict[file_name] = df

        return {"message": f"文件{file_name}已上传"}

    @app.get("/files/")
    async def get_files():
        """
        此函数返回所有已上传文件的名称列表。
        """
        return {"files": list(df_dict.keys())}

    @app.get("/downloadfile/{file_name}")
    async def download_file(file_name: str):
        """
        此函数管理文件的下载。
        将特定文件的内容转换为Json格式后返回
        """
        df = df_dict.get(file_name, pd.DataFrame())
        return JSONResponse(content=df.to_json(orient="records"))

    @app.post("/execute_code/")
    async def execute_code(request_body: dict):
        """
        此函数负责执行用户在请求体中提供的Python代码。
        最终,它将以Json格式返回结果
        """
        # 初始化局部字典
        local_dict = {}
        code = request_body.get("code")
        try:
            exec(code, df_dict, local_dict)
            result_dict.update(local_dict)

            # 将任何非json序列化的项目转换为字符串,以确保能通过API发送,因为不能直接发送数据框回去
            for k, v in local_dict.items():
                if isinstance(v, pd.DataFrame):
                    local_dict[k] = v.to_json()
            local_dict = {k: str(v) if not isinstance(v, (int, float, bool, str, list, dict, pd.DataFrame)) else v for k, v in local_dict.items()}

        except Exception as e:
            raise HTTPException(status_code=400, detail=str(e))

        # 将局部字典转换为JSON格式的字符串
        local_dict = json.dumps(local_dict)
        return local_dict

    @app.get("/clear_state/")
    async def clear_state():
        """
        此函数管理字典的重置,以避免干扰先前上传的文件。
        """
        df_dict.clear()
        result_dict.clear()
        return {"message": "状态已清空"}

你可能想知道为什么我们要用FastAPI来执行代码,而不是直接在本地用exec方法运行。答案是,我们不想在自己的机器上运行可能不安全的代码。相反,我们会在另一个系统(比如另一台电脑或容器)上执行代码,这样即使出现问题也不会影响我们的主环境。FastAPI允许我们将代码和数据发送到这个独立系统,并通过API安全接收结果。

要运行这个应用,请在命令行中输入以下命令:

    uvicorn main:app --host 0.0.0.0 --port 8000 --reload
步骤 4:创建一个 Python 的封装

虽然你可以直接使用 requests.postrequests.get 调用 API,但创建一个 Python 封装器来将这些调用抽象为方法会更加简洁优雅。请将以下代码复制到 session.py 文件中:

    # 文件名: session.py  
    import requests  
    import pandas as pd  
    from typing import Any  
    from io import StringIO  

    class CodeSession:  
        def __init__(self, url: str) -> None:  
            self.url = url  

        def upload_file(self, file_name: str, data: pd.DataFrame) -> Any:  
            # 将数据转换为CSV字符串(忽略索引)  
            data_str = data.to_csv(index=False)  
            files = {"file": StringIO(data_str)}  
            return requests.post(f'{self.url}/uploadfile/{file_name}', files=files).json()  

        def get_files(self) -> Any:  
            # 获取文件列表  
            return requests.get(f'{self.url}/files/').json()  

        def download_file(self, file_name: str) -> pd.DataFrame:  
            # 下载文件并解析为DataFrame  
            response = requests.get(f'{self.url}/downloadfile/{file_name}').json()  
            return pd.read_json(response)  

        def execute_code(self, code: str) -> Any:  
            # 执行给定的代码并返回JSON响应  
            return requests.post(f'{self.url}/execute_code/', json={"code": code}).json()  

        def clear_state(self) -> Any:  
            # 清除会话状态并返回响应  
            return requests.get(f'{self.url}/clear_state/').json()

这个工具使得以更符合 Python 风格的方式与沙箱应用交互变得更加容易,让你能够专注于编写和测试代码,而无需处理繁琐的 API 调用。

第5步:测试沙盒app

为了确保我们的FastAPI沙盒应用运行正常,我们需要测试它。测试的方法如下:

把下面这段代码复制到一个叫做 test_session_and_main.py 的文件里

    # 文件名 test_session_and_main.py

    import sys
    from session import CodeSession
    import pandas as pd

    # 从命令行参数中获取端口号
    if len(sys.argv) != 2:
        print("用法: python test_session_and_main.py <端口>")
        sys.exit(1)

    port = sys.argv[1]  # 从系统参数中获取端口号
    base_url = f'http://127.0.0.1:{port}'
    session = CodeSession(base_url)

    # 准备一些示例数据
    data = pd.DataFrame({
        'A': [1, 2, 3],
        'B': [4, 5, 6]
    })

    # 上传一个文件到会话中
    upload_resp = session.upload_file('df', data)
    print("上传响应:", upload_resp)

    # 使用会话获取文件列表
    files = session.get_files()
    print("沙箱中的文件列表:", files)

    # 从会话中下载一个文件
    downloaded_df = session.download_file('df')
    print("下载的数据框如下:")
    print(downloaded_df)

    # 在服务器上执行一些代码
    result = session.execute_code("result = df['A'].sum()")
    print("执行结果:", result)

运行脚本(我们可以使用 cd .. 命令切换到父目录):

运行 python test_session_and_main.py 8000

这将与本地运行的FastAPI应用程序进行互动,你应该看到每个步骤(上传、列出、下载和执行代码)都正常工作的确认信息。

通过运行此测试,你将验证你的FastAPI应用按预期工作,并准备好进一步使用和部署。

绕道-容器是什么?

容器是一种轻量级、独立且可执行的软件单元,包含运行该软件所需的所有内容,如代码、运行时环境、工具、库和配置。容器将软件与其运行环境隔离,确保软件在各种操作系统和基础设施上都能一致运行。

为什么需要虚拟化呢?

虚拟化允许在一台物理机上运行多个操作系统或应用程序,通过创建硬件、软件或存储设备的虚拟版本,从而将应用程序彼此隔离,减少冲突的可能性并增强安全性。

为什么虚拟化对我们来说很重要?

对于我们来说,虚拟化是非常关键的,因为我们需要运行潜在不安全的代码。如果没有虚拟化,直接在你的机器上运行这些代码的话,可能导致系统崩溃、数据丢失或其他严重的后果。通过容器技术实现的虚拟化可以让我们隔离执行环境,确保任何有害的影响都局限于虚拟环境内,不会波及到你的主系统。

什么是图片?

一个镜像文件是容器的蓝图文件或快照文件。它类似于蓝图或快照,包含了运行应用程序所需的一切,包括操作系统、应用代码、库和依赖项等。镜像文件一旦创建就不会更改。它们可以被存储和重复利用,确保在不同环境中的一致性

图像分层创建和结构是怎样的?

一个 Docker 镜像是通过编写一个名为 Dockerfile 的文件来构建的,这是一个包含一系列用于构建镜像的命令的脚本。这些命令可能包括复制文件、设置环境变量、安装软件包等。Dockerfile 中的每条指令都会生成镜像的一个新层。这些层会逐层叠加在一起,Docker 会缓存这些层来加快构建速度。如果镜像的任何部分发生变化,只需重建受影响的层即可,从而节省时间和资源。

容器是怎么运行镜像的呢?

当你运行一个容器时,它会基于镜像创建一个实例。这意味着容器是该镜像的运行版本,拥有自己独立的文件系统、网络接口和进程空间,但它仍然使用宿主操作系统的内核。容器之所以轻便,是因为它们不需要一个完整的操作系统;它们只需要镜像中定义的资源。

在 Kubernetes Pod 中使用镜像

Kubernetes 是一个强大的编排工具,用于管理容器化应用。它允许你部署、扩容和管理容器在多台机器组成的集群中。在 Kubernetes 中,Pod 是你可以创建或部署的最小且最简单的单元。一个 Pod 可以包含一个或多个容器,所有容器共享相同的网络和存储资源。

当你在 Kubernetes Pod 中部署一个镜像文件时,Kubernetes 会将其调度到集群中的某个节点,并确保它有足够的资源运行。如果容器出现故障,Kubernetes 可以自动重启它,甚至用基于同一镜像的新容器替换它,从而在生产环境中提供强大的扩展性和可靠性。

为什么要把我们的FastAPI应用移到容器?

注:原文末尾的问号在Markdown格式下通常会自动生成相应的排版效果,因此省略了中文翻译中的问号以保持一致。但为了保证语句完整,此处保留了问号。在实际应用中,可以根据具体Markdown解析器的行为来决定是否保留末尾的标点符号。

我们已经建立了一个FastAPI应用,包含上传及下载文件、执行代码的任务、以及清除状态的任务。然而,我们目前是在本地机器上运行此应用,这意味着我们仍然暴露在之前提到的风险中。

要真正实现一个安全的沙盒(一个隔离的测试环境),我们需要将这个 FastAPI 应用打包到一个容器中。Docker 容器就像你机器里的一个独立小环境。在该容器中运行可能不安全的代码不会损害你的主系统。即使容器损坏,也可以通过从同一镜像重新创建它来轻松恢复。此外,在生产环境中,有自动化系统可以修复或替换受损的容器,确保最小的停机时间并保持一致的性能。

步骤 6:写 Dockerfile

一个 Dockerfile 实际上就像一个配方,告诉 Docker 如何为我们应用构建镜像。这是我们为沙箱应用使用的 Dockerfile:

    # 注释:Dockerfile 文件名  
    FROM python:3.11-slim  # 从 python:3.11-slim 镜像构建  

    WORKDIR /app  # 设置工作目录为 /app  
    COPY requirements.txt requirements.txt  # 复制 requirements.txt 文件  
    RUN pip3 install -r requirements.txt  # 运行命令安装依赖  
    COPY . .  # 复制当前目录下的所有文件到容器内  
    CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]  # 设置启动命令

剖析 Dockerfile

  • **FROM python:3.11-slim**: 这行设置了基础镜像为 Python 3.11 版本。slim 版本是一个只包含必需组件的精简镜像,使其轻量且高效。

  • **WORKDIR /app**: 这行创建并设定了容器内的工作目录,后续命令都将在此目录中执行。

  • **COPY requirements.txt requirements.txt**: 这行将 requirements.txt 文件从本地复制到容器中。

  • **RUN pip3 install -r requirements.txt**: 这行在容器内安装 requirements.txt 中列出的所有必需的 Python 包。

  • **COPY . .**: 这行将应用程序的其余文件复制到容器中。

  • **CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]**: 这行指定了使用 Uvicorn 启动 FastAPI 应用的命令,并使其在容器的 8000 端口上监听。

通过这个Dockerfile,我们可以创建包含我们整个FastAPI应用的镜像,完全自包含且准备好运行。

步骤 7:构建和运行容器

接下来我们来制作Docker镜像,然后在容器中运行它。

注意:在这里,我们使用了nerdctl,它是 containerd 的命令行接口。Containerd 是一个轻量级的容器运行时,负责处理运行和管理容器的底层细节。我通过 Rancher Desktop 安装了 nerdctl。或者,你也可以使用 Docker,它为容器管理提供了用户友好的方式。如果你使用的是 Docker Desktop 而不是 Rancher Desktop,那么请将所有的 nerdctl 命令替换为 docker 命令。

构建镜像(步骤,在进入沙盒目录之后)。

在终端中运行以下命令:

nerdctl build -t code_session .

这个命令基于名为code_session的Dockerfile创建镜像

运行容器:

nerdctl run -d -p 8080:8080 --name sandbox code_session
# 执行此命令会运行一个名为'sandbox'的容器,使用`nerdctl`并将主机的8080端口映射到容器的8080端口。

此命令启动一个名为 sandbox 的容器,基于 code_session 镜像。它将容器内的 8000 端口映射到您机器上的 8080 端口,从而使您能够通过 http://localhost:8080 访问应用程序。

第8步:监视容器

如果你想查看日志或了解容器内的情况,可以试试下面这个命令。

    nerdctl 日志 -f sandbox

这将展示 sandbox 容器的实时日志。

直接拉镜像

如果你不想自己动手构建镜像,可以直接从Docker Hub: 拉取并直接运行预构建的镜像。

拉取图像:例如,你可以使用命令 docker pull 来拉取镜像。

    使用 `nerdctl pull shrishml/code_session` 拉取镜像

试试启动容器:

运行以下命令来启动容器:运行 nerdctl run -d --name sandbox -p 8080:8000 shrishml/code_session。将容器的8000端口映射到主机的8080端口。

就这样,你搞定啦!你现在有了一个完全可用的沙盒环境,安全地运行在Docker容器中。

测试 FastAPI 沙箱应用,你可以运行或输入以下命令,例如:[命令],

运行 python test_session_and_main.py 8080 来启动服务并监听端口8080.

我们使用的是端口 8080,它连接到运行你 FastAPI 应用的 Docker 容器的 8080 端口。这与你在本地运行 FastAPI 应用时可能使用的端口 8000 不同。因为你已经在容器上用端口 8080 设置了应用,所以可以关闭之前在端口 8000 上运行的 FastAPI 应用实例。

此命令将与容器内的FastAPI沙盒应用程序进行交互,允许你在指定的端口上测试文件的上传、下载以及代码的执行。

感谢你的时间,希望这能帮助你更好地理解代码解释工具以及一些关于容器和API的基础知识。

如果你觉得这篇文章不错,我邀请你关注我,未来会有更多精彩内容。此外,你也可以在我的LinkedIn上与我联系。

你可能会喜欢我的其他相关文章。

最近,我一直研究大型语言模型,比如ChatGPT,并发现它们本质上是预测……之类的。medium.com
打破内存限制:ZeRO 如何革新大型语言模型的训练,训练像 GPT-3 这样的强大语言模型是一项资源密集型任务。据估计……medium.com
LoRA:从基本原理出发看低秩适应在所有深度学习模型的中心,都有一系列的矩阵乘法,其间穿插着各种操作等……medium.com
打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP