totally_safe_transmute,逐行

2021-03-17 05:07:59

这些文件链接(由Benjamin Herr编写)在推文的声音中实现厌恶生锈的std :: mem :: mem :: transfultwithout的任何使用不安全。如果你经营它,你会发现它确实有效!

#[test] fn main(){设v:vec< U8> = B" foo" .to_vec();设v:string = totally_safe_transmute(v); assert_eq! (& v," foo"); }

$ git clone https://github.com/ben0x539/totaly- safe-transmute $ cargo build $ cargo测试编译完全安全 - 传输v0.0.3(/ tmp /完全安全传输)完成测试[未通过+ debuginfo] Target(s)在0.49s运行目标/ debug / deps / totally_safe_transmute-be2ea6d9a3f8d258 running 1 testtest main ...确定测试结果:好的。 1通过; 0失败; 0忽略了; 0测量; 0过滤出来;完成在0.00s Doc-Tests完全安全 - 透射术0测试结果:OK。 0通过; 0失败; 0忽略了; 0测量; 0过滤出来;完成0.00秒

此博客文章将通过该实施,逐行,并解释它的工作原理。没有什么特别复杂的;我刚刚出现了一个巨大的东西,想知道我会提供详细的解释。 Rust Newcomearsare预期的受众。

大多数不安全的语言都有用于在某些内存地址处传输(或重新诠释)数据的机制,作为全新类型。 C在其铸造语法下包括诠释; C ++提供了更明显的重新诠释器< t>(当reinterpret_cast熟悉时,有很多警告发布)。

C样式“通用”API通常会产生VOID *的形式产生的结果,呼叫者预计将Void *施放到合适的类型.Callers负责确保目的地类型与类型相同或兼容最初是施加到空白*。

C和C ++回调模式经常提供void *参数,允许用户在回调之间提供其他数据或上下文。然后,每个呼叫负责铸造到适当的类型。

指针值偶尔需要1通过ANINTEGLAL类型循环。 C ++专门允许这一点,只要目的地积分型至少具有足够的宽度来表示所有可能的指针值。

多态性:Berkeley套接字Apispecifies Connect(2)接受结构SockAdd *,它实际上是在Internally中重新解释的家庭特定的SockAddr结构之一(例如IPv4Sockets的Sockaddr_in)。 C ++还明确允许在其“相似性”规则下。

廉价的对象序列化或转换:与上面相关,但略微有关:C和C ++与您转换几乎任何ObjectOto Char * 2.这允许对象被视为字节的袋子,这在编写哈希表时是方便的(您不在乎内容内容,您只想唯一地识别它们)或序列化结构在特定于主机的格式3时。

以上每个都是有用的,但令人难以置信的不安全:嬗变是在运行时诺兰操作,该运行时将一个类型变为另一个类型,而是在编译时将某些位置视为在内存中的某个位置,就像其类型一样。结果:TypesResult之间的大多数可能是未定义的行为。

Rust需要与C接口,所以4 RUTR支持嬗变。它通过std :: mem :: transtute来实现。但是,转换了一个根本不安全的操作,因此Rust禁止使用MEM :: Transume Event for在明确的不安全上下文中:

使用std :: mem; # ){让foo = foo {a:0xaa,b:0xbb,c:0xcc,d:0xdd};让吧:栏=不安全{MEM :: Transume(Foo)}; //输出(x86-64):bar.a = 0xddccbbaa println! (" bar.a = {:x}",酒吧.a); }

当然,传输可以包裹到安全的上下文中。但是底层操作总是不安全,不可能在其他安全的铁锈代码中。

#![foutbid(不安全_code)]使用std :: {io :: {self,write,seek},fs}; pub fn totally_safe_transmute< t,u> (v:t) - > u {#[R​​epre(c)]枚举e< t,u> {t(t),#[允许(dead_code)] u(u),}让v = e :: t(v);让mut f = f = fs :: OpenOptions :: new().write(true).open(" / proc / self / mem").expect(" welp"); F .seek(IO :: Seekfrom :: start(& v作为* const _为u64)).expect(" Oof"); f .write(& [1]).expect(" darn");如果让e :: u(v)= v {return v; } 恐慌! (" RIP"); }

禁止是控制RUSTC LINTER(以及允许,警告和拒绝)的属性。在这种情况下,我们会告诉Rustc禁止禁止浏览不安全的嘴唇的任何东西,这正是它在TIN上所说的内容:捕获使用不安全。

在这种情况下,禁止使用不安全不做任何事情:快速阅读代码展示不安全从未显示过。但它是读者的顶级证明,如果rustc接受代码(并且它确实),那么没有使用不安全。

然后它采用一个混凝土参数v,它是T型而最终,ITRETURNS A U.

我们知道,嬗变功能的作业是将一种类型的SomeValue重新诠释为其他类型,因此我们可以将此签名重写为:

我们的下一点是一个有一些时髦的属性的简洁枚举。用我们友好的typeparameters重写:

首先,我们将E作为ROP(C)标记为Rep(c)。这是ABI-Modify属性:它告诉RustC使用平台的C abi而不是(故意)的不稳定生锈abi。

这实际上是什么意思?对于具有字段的枚举(如此),RUST使用“标记的Union”表示形式。在效果中,E成为这样的东西(在C语法中):

下一页:E有两个变体:第一个包含SRCTY类型的值,另一个包含一个值。

可是等等!另一个rustc linter注释:这次,我们告诉Rustc,你的变体无法失败的Dead_code Lint.Normally,RustC会在静态推断上方的情况下警告我们;使用DEAD_CODE启用,它静默警告。像ABI布局一样,我们会看到为什么这是重要的。

最后,我们通过新的绑定遮蔽了我们的v参数。 v已经是T型,Socreating e :: t完全没有问题。

首先,我们正在打开一个文件。具体而言,我们在写模式下打开/ proc / self / mem。

/ proc / self / mem是一个非常特殊的5个文件:它介绍了当前进程的视图,虚拟地址范围稀疏地映射。

作为一个快速的黑客,我们可以通过检查str对象6的内存表示来证明这一点在Python中:

>>> x ="此字符串足够长,以防止任何字符串互动" >>> #在cpython中,对象' s id(通常)它的指针>>> x_addr = id(x)>>>十六进制(X_ADDR)' 0x7FF1BC7CFCE0' >>> mem =打开(" / proc / self / mem" mode =" rb")>> mem。寻求(x_addr)>>> mem。读(Len(x)* 4)B' [Snip]" thi \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00这个字符串足以阻止任何字符串interning \ x00 e \' [Snip]'

>>> #使用CTYPE避免我们看到的布局拍白,而GT;>>进口ctypes>>> cstr = ctypes。 c_char_p(b"看起来ma,没有手和#34;)>> cstr_addr = ctypes。 CAST(CSTR,CTYPES。C_VOID_P)。价值>>>十六进制(CStr_addr)' 0x7f47f3e9c7​​90' >>> MEM =打开(" / proc / self / mem",mode =" r + b")>>> mem。寻求(cstr_addr)>>> mem。读(len(cstr.value))b'看起来ma,没有手' >>> mem。寻求(CStr_Addr + 5)>>> mem。写(' p')>> mem。寻求(cstr_addr)>>> mem。读(Len(CSTR.值))B'看PA,没有手和#39;

完全两块完全_safe_transmute现在应该是有意义的:我们在自己的运行过程中寻求我们的V变量(现在是e的变体)的地址,我们为它写了一个U8([1])。

但为什么1?回想起上面的e的c abi表示!第一部分是iurunion判别者。当数据是SRCTY时,判别是0.当我们强制性地搬运到1时,数据现在被解释为DSTTY!

好的,所以我们已经戳了记忆并将我们的E :: T转变为E :: U。让我们看看我们是如何让它的:

乍一看,这对此没有什么特别之处:我们只是丢弃了我们之前添加的枚举包装,以便我们可以返回我们的新棉被的dstty值。

......但是返回dstty的唯一方法是v为e :: u。

......但V无条件初始化为E :: T,因此从未达到过返回。

这就是我们需要允许(Dead_Code)之前的原因:没有E :: U曾经以那种可能达到返回声明的方式构建,因此只不需要它作为一个变种。确实,我们可以通过删除来确认这一点允许属性:

$ Cargo Build Compling完全安全 - 传输V0.0.3(/ TMP /完全安全 - 传输)警告:Variant永远不会构建:`U` - > SRC / lib.rs:9:9 | 9 | U(U),| ^^^^ | =注意:`#[Warn(Dead_Code)]`On默认运行:1个警告发出的完成Dev [未通过+ DebugInfo]目标0.15s

但是,唉:这不是真正死的代码:编译器是“错误的”,我们通过修改程序自己的内存来在运行时生成存在。然后我们是不可能的条件,并返回我们的传输价值。

totally_safe_transmute是一个令人愉快的黑客,演示了当推理程序的行为时的关键限制:每个行为模型都是在环境模型上的偶然,并在程序(或程序的运行时,或编译器,或其他任何其他人)选择(或未切断)在所述环境中处理看似不可能的条件。

这样做的能力不会反映生锈的根本不良,任何不仅仅是它的安全语言:从Rust的角度来看,完全_Unsafe_Transmute是什么,因此undefined是不可义的;处理无法发生的事情。

如前所述,这种黑客仅通过对/proc/self/mem的依赖性作用于Linux。其他操作系统可能具有类似的机制。

我没有测试过这个,但我很确定它只适用于小型架构(如x86).on大endian架构,写入可能需要调整写作。

如果我们非常迂腐:这技要技术上不是嬗变。语义上,嬗变是编译时类型的无效变化; totally_safe_transmutewrites程序的内存内存表示,以在运行时完成等效行为。我认为这是一个有区别的区别。

因为Totally_Safe_Transmute依赖于未定义的行为(不可能的程序状态),因此在擦除E :: U分支并将功能缩短到Anuncuditional Punic的功能,Rust将是正确的!它在我的测试中没有这样做(即使在释放模式下),但程序语义中没有任何东西可以防止它这样做。但是,它会的日子,而且完全_safe_transmute会停止工作!

应用程序和网络编程的巨大罪行之一,以及漏洞的共同来源。 ↩

除其他原因之外。如上所述,当所有需求是某些对象的“一对字节”视图时,嬗变是有用的,或者当您可以保证一致的类型布局时。它对先进的寿命抹布也很有用。 ↩

特定于平台:它是Linux的Procfs的一部分。因此,Totally_Safe_Transmute将无法在其他内容上运行(AS-IS)。 ↩

正如您所明白的那样,它不是微不足道的(它不仅仅是一个长度数据对)。 unicodeObject.h中的CPython源具有完整的结构细节,完全与此帖子无关紧要。 ↩

Reddit讨论