简介
在OCaml堆中不要存储任何机密信息。因此,您绝不能将机密复制到任何OCaml堆分配的值中,因此,即使是暂时使用的Bytes、Strings或Arrays也不能使用。
OCaml内存模型介绍
OCaml值统一以标记机器字表示。字的最低有效位用作标记,区分指针(tag = 0)和立即数值(tag = 1)。因此,一个值始终具有固定大小,并且是指针或立即数值。
立即数值将其数据存储在字的最高有效部分,即32位系统中为31位,在64位系统中为63位。指针将其数据存储在块中,这些块位于所谓的OCaml堆中。OCaml堆是 Garbage Collector (GC) 管理的一组块。块是带有头的数据块。头指定数据的大小以及GC使用的其他元信息。块可以包含OCaml值(指针或即时数值)或不透明数据。
总之,所有OCaml值都表示为机器字,可以直接将数据存储在字中,也可以是指向堆分配的块的指针。每个指针指向一个且仅一个块。多个指针可以指向同一个块。这些值被认为是物理相等的。一些块没有被任何指针指向。这些块称为死块,并由GC回收。
OCaml垃圾收集器介绍
GC通过分配、移动和释放它们来管理块。GC本身使用arena,该arena可以从C内存分配器(malloc)或通过memmap系统调用直接从内核中获得(取决于特定系统和运行时)。
GC是分代的,这意味着值首先分配在堆的一个特殊区域中,称为小堆。小堆是一个固定大小的连续内存区域,在运行时用三个指针表示:beg
指向小堆的开头,end
指向小堆的结尾,cur
指向小堆自由区域的开头。当分配一个块时,cur
增加块的大小。然后使用数据初始化该块。当小堆没有更多的自由空间(即end-cur
小于所需块的大小)时,会触发一个小GC循环。GC分析存储在小堆中的所有块,并将至少被一个指针引用的所有块复制到大堆中。之后,将cur
指针设置为beg
。
在大堆中,一个块也可能在称为压缩的过程中被复制多次。压缩器可以尝试重新排列其区域中的块,以实现更紧凑的堆表示。
安全后果
由于OCaml GC是移动式GC,它可能任意复制堆分配的数据。虽然它被称为移动,但实际上仍然只是复制。即,当一个块从小堆移到大堆时,实际上只是位复制,因此会复制一份。小堆中的虚拟块可能会存活任意长的时间,直到被某个新分配的值覆盖。当对象在压缩过程中移动时,它也会被复制,并且在该过程中可能或可能不会被覆盖。当然,不言而喻,一旦一个块变成死块,它仍然可能在堆中存活任意长的时间,直到被GC重用。
所有这些意味着,如果一个密钥最终存储在OCaml堆中,它将变得不可控,因为GC可以以任意和相对不可预测的方式复制它。因此,我们只能将密钥存储在立即值或不受GC控制的区域中。正如之前所说的,所有指针类型的OCaml值总是指向OCaml堆中的一个块。一个块可能直接包含数据,也可能包含一个指针本身,该指针将指向内存堆之外。所谓的自定义块可能会或可能不会将它们的信息存储在OCaml堆中,这取决于每个自定义块的特定表示。例如,Bigarray库提供了存储负载在OCaml堆之外的自定义块。因此,Bigarray是一个自定义块,有两个字段:指针和大小。它是一个不透明的块,即GC永远不会将这两个值视为OCaml值,并且永远不会跟随大小或指针。指针指向的数据位于OCaml堆之外,并且可以由malloc或memmap分配(实际上,它可以是任意整数,甚至指向堆栈、静态数据,这并不重要,只要我们将bigarrays视为ptr,len配对即可)。
所有这些使得Bigarrays非常适合存储机密信息。我们可以确信它们不会被GC移动,我们可以在它们被释放后覆盖它们以防止信息泄漏。
进一步考虑
我们应该谨慎,绝不能允许将秘密从安全的地方复制到OCaml堆中。这意味着,即使我们的主要存储是一个安全的bigarray,如果我们将其内容复制到OCaml字符串中,信息仍然会泄漏。因此,如果我们首先将信息读入OCaml字符串,然后再将其复制到bigarray中,信息仍然会泄漏。因此,任何使用OCaml堆分配值的接口都是不安全的,不应使用。例如,我们不能使用OCaml通道读取或写入秘密(我们应该依赖Unix模块提供的内存映射或无缓冲IO)。同时,每当你从Bigarray获取string
数据类型时,你都会得到复制的数据,带来了所有的影响。