幽灵 C++
Spooky C++

原始链接: https://www.nablag.com/spooky_cpp

一个示例 C++ 程序似乎只在使用 `-O2` 编译时授予 root 权限,这是未定义行为导致的,而非超自然原因。`Sanitize` 返回的 `std::string_view` 引用了由输入创建的临时 `std::string` 的缓冲区;调用结束后,该临时对象被销毁,导致视图悬垂。将悬垂视图与 `admin_id` 比较是未定义行为,优化后的编译器可能产生令人惊讶的 `true` 结果。这个问题很脆弱:它依赖于长度匹配和内联参数,并在存储输入或使 `admin_id` 为全局变量等小重构后消失。教训是避免悬垂字符串视图,并谨慎处理临时对象生命周期;编译器优化并不“闹鬼”。

Hacker News 上的讨论串讨论了一篇关于诡异 string_view bug 的 C++ 博客文章。该文章似乎从临时 std::string 返回 string_view,导致悬垂视图和未定义行为。评论者解释,string_view 是非拥有引用,复制它复制的是引用而非数据;将字符串字面量/临时对象传给 Sanitize 会创建一个随后被销毁的临时 std::string,导致返回的视图悬垂。有人建议不要传递临时对象,使用 ASan/UBSan 等 sanitizer 捕获 stack-use-after-scope,并争论 string_view 是否应仅作为参数使用,而不应返回或存储。其他人指出 admin_id 和 "foobarbazboo" 长度相同,导致输出令人困惑。该讨论串突出了 string_view 周围的 C++ 生命周期陷阱。
相关文章

原文

You want to see something… spooky? Do you want to hear a story, so haunting, so bone-chilling, it keeps you up at night? Well, let’s look at this program that grants permissions:

#include <iostream>
#include <string>
#include <string_view>

std::string_view Sanitize(const std::string& input) {
  std::string_view sanitized = input;
  while (!sanitized.empty() && sanitized.front() == ' ') { 
    sanitized.remove_prefix(1);
  }
  return sanitized;
}

int main() {
  std::cout << std::boolalpha; 
  std::string_view username = Sanitize("foobarbazboo");
  std::string admin_id = "root_1231231";
  std::cout << "Grant foobarbazboo root permissions = " << (username == admin_id) << "\n";
  return 0;
}

When I compile and run this code (clang++-19 -std=c++17 -O0 -Wall -Wextra -Wpedantic cursed.cpp -o cursed_clang && ./cursed_clang), everything works fine and my user foobarbazboo does not get root permissions:

Grant foobarbazboo root permissions = false

However, if I compile this code in a forest, at midnight on halloween, on top of a Ouija board, after saying “Candyman” in front of a mirror 5 times, and changing the optimization flag to -O2, this program will mysteriously grant foobarbazboo root permissions…

clang++-19 -std=c++17 -O2 -Wall -Wextra -Wpedantic cursed.cpp -o cursed_clang_optimized && ./cursed_clang_optimized

Grant foobarbazboo root permissions = true

Is this the work of g-g-ghosts? Is my computer h-h-haunted?

  • No - you can reproduce this on your computer as well.

Is C++17 c-c-cursed?

  • Yes, but not by the supernatural. This can happen on C++20 and C++23 as well:
clang++-19 -std=c++20 -O2 -Wall -Wextra -Wpedantic cursed.cpp -o cursed_clang_c20 && ./cursed_clang_c23

Grant foobarbazboo root permissions = true

clang++-19 -std=c++23 -O2 -Wall -Wextra -Wpedantic cursed.cpp -o cursed_clang_c23 && ./cursed_clang_c17

Is my compiler c-c-cursed?

  • No, because GCC will do the same thing:
g++ -std=c++17 -O0 -Wall -Wextra -Wpedantic cursed.cpp -o cursed_gcc && ./cursed_gcc
Grant foobarbazboo root permissions = false

g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic cursed.cpp -o cursed_gcc_optimized && ./cursed_gcc_optimized
Grant foobarbazboo root permissions = true

As you can guess, this is an example of undefined behavior causing unintuitive results.

Even though this is a funny example, I doubt a bug like this will surface in the wild (nor be easily exploited) as this behavior is incredibly hard to reproduce.

For example, this only works because the string foobarbazboo is the same length as admin_id, and is passed as an inline parameter. If you change the input length or refactor the program to store foobarbazboo in a temporary variable, the behavior goes away.

Even if you didn’t know anything about UB, I think most people would think this code is ill-formed and weird. A handful of readability/refactoring principles will eliminate this particular behavior. For instance, refactoring admin_id to be a singleton/global variable will make the program behave correctly.

Anyway, Happy Halloween

uname -r
6.18.54.1-microsoft-standard-WSL2
clang++ -v
Debian clang version 19.1.7 (3+b1)
Target: x86_64-pc-linux-gnu
Debian clang version 19.1.7 (3+b1)
g++ -v
Target: x86_64-linux-gnu
# ... 
gcc version 14.2.0 (Debian 14.2.0-19)
联系我们 contact @ memedata.com