diff --git a/1.8.7-p374 b/1.8.7-p374 index ba3d8d6..e333dba 100644 --- a/1.8.7-p374 +++ b/1.8.7-p374 @@ -38,7 +38,15 @@ install_bundler() { "$PREFIX_PATH"/bin/gem install bundler -v 1.17.3 } -RUBY_CFLAGS="" +# -O3 because leaving this empty does not mean "use configure's default". ruby-build +# exports it as CFLAGS, which supersedes configure's own optflags in the compile line, so +# an empty value builds at -O0 — measured 2-3x slower than the same source at -O3. +# +# -fno-strict-overflow is mandatory here, not belt-and-braces. 1.8.7's fixnum overflow +# checks assume signed overflow wraps, which is undefined behaviour that GCC exploits from +# -O2 upward: without this flag a -O3 build silently evaluates 2**64 to 0 and types it +# Fixnum. Wrong arithmetic, no warning, no crash. It costs nothing measurable. +RUBY_CFLAGS="-O3 -fno-strict-overflow" case "$(cc -v 2>&1)" in *gcc*) RUBY_CFLAGS+=" -Wno-discarded-qualifiers" diff --git a/1.9.3-p551 b/1.9.3-p551 index 6b884ec..fae5396 100644 --- a/1.9.3-p551 +++ b/1.9.3-p551 @@ -19,7 +19,15 @@ install_bundler() { "$PREFIX_PATH"/bin/gem install bundler -v 1.17.3 } -RUBY_CFLAGS="" +# -O3 because leaving this empty does not mean "use configure's default". ruby-build +# exports it as CFLAGS, which supersedes configure's own optflags, so an empty value builds +# at -O0 — measured 2-3x slower than the same source at -O3. +# +# -fno-strict-overflow guards the fixnum overflow checks in these old sources, which assume +# signed overflow wraps — undefined behaviour that GCC exploits from -O2 up. On 1.8.7 its +# absence silently makes 2**64 evaluate to 0. Kept uniform across every definition here; +# it costs nothing measurable. +RUBY_CFLAGS="-O3 -fno-strict-overflow" case "$(cc -v 2>&1)" in *gcc*) RUBY_CFLAGS+=" -Wno-discarded-qualifiers" @@ -28,6 +36,16 @@ case "$(cc -v 2>&1)" in RUBY_CFLAGS+=" -Wno-return-type" # GCC 15 porting: Use -std=gnu99 with relaxed type checking RUBY_CFLAGS+=" -std=gnu99 -Wno-error=implicit-function-declaration" + # Cap _FORTIFY_SOURCE at 2. Ubuntu's GCC defines it to 3 automatically whenever + # optimization is on, and level 3's more aggressive object-size inference trips on + # 1.9.3: the build aborts with "*** buffer overflow detected ***" while running the + # freshly built miniruby to generate exts.mk. Level 2 builds and runs clean. + # + # This is not a loss of hardening relative to before. _FORTIFY_SOURCE does nothing + # without optimization, so while this definition was (unintentionally) building at + # -O0, no fortification was happening at all. Only 1.9.3 needs the cap; the other + # definitions build fine at level 3. + RUBY_CFLAGS+=" -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2" ;; *clang*) RUBY_CFLAGS+=" -Wno-compound-token-split-by-macro" diff --git a/2.3.3 b/2.3.3 index 87569aa..bdeb5bb 100644 --- a/2.3.3 +++ b/2.3.3 @@ -12,7 +12,16 @@ install_bundler() { "$PREFIX_PATH"/bin/gem install bundler:1.17.3 } -RUBY_CFLAGS="" +# -O3 because leaving this empty does not mean "use configure's default". ruby-build +# exports it as CFLAGS, which supersedes configure's own optflags, so an empty value builds +# at -O0 — measured 2-3x slower than the same source at -O3. (rbconfig still reports +# optflags: -O3 in that case, which is misleading; the timings are what to trust.) +# +# -fno-strict-overflow guards the fixnum overflow checks in these old sources, which assume +# signed overflow wraps — undefined behaviour that GCC exploits from -O2 up. On 1.8.7 its +# absence silently makes 2**64 evaluate to 0. Kept uniform across every definition here; +# it costs nothing measurable. +RUBY_CFLAGS="-O3 -fno-strict-overflow" case "$(cc -v 2>&1)" in *gcc*) RUBY_CFLAGS+=" -Wno-discarded-qualifiers" diff --git a/2.3.8 b/2.3.8 index ab9a9f5..a39620f 100644 --- a/2.3.8 +++ b/2.3.8 @@ -12,7 +12,16 @@ install_bundler() { "$PREFIX_PATH"/bin/gem install bundler:1.17.3 } -RUBY_CFLAGS="" +# -O3 because leaving this empty does not mean "use configure's default". ruby-build +# exports it as CFLAGS, which supersedes configure's own optflags, so an empty value builds +# at -O0 — measured 2.6-3.5x slower than the same source at -O3. (rbconfig still reports +# optflags: -O3 in that case, which is misleading; the timings are what to trust.) +# +# -fno-strict-overflow guards the fixnum overflow checks in these old sources, which assume +# signed overflow wraps — undefined behaviour that GCC exploits from -O2 up. On 1.8.7 its +# absence silently makes 2**64 evaluate to 0. Kept uniform across every definition here; +# it costs nothing measurable. +RUBY_CFLAGS="-O3 -fno-strict-overflow" case "$(cc -v 2>&1)" in *gcc*) RUBY_CFLAGS+=" -Wno-discarded-qualifiers" diff --git a/2.5.9 b/2.5.9 index 24e256c..0b5c673 100644 --- a/2.5.9 +++ b/2.5.9 @@ -3,7 +3,15 @@ install_bundler() { "$PREFIX_PATH"/bin/gem install bundler:2.3.27 } -RUBY_CFLAGS="-O3 -march=native" +# -march=native was here and has been dropped: measured ~12% *slower* than plain -O3 on +# Zen 4, worst on numeric loops (-16% integer, -21% float). Microbenchmarks are the +# friendliest case for it, so there's no reason to expect a win on real work either. +# +# -fno-strict-overflow guards fixnum overflow checks that assume signed overflow wraps — +# undefined behaviour GCC exploits from -O2 up. 2.5 uses builtin overflow intrinsics and +# doesn't strictly need it, but it's free (within noise here) and keeps every definition +# in this repo on the same flags. +RUBY_CFLAGS="-O3 -fno-strict-overflow" case "$(cc -v 2>&1)" in *gcc*) RUBY_CFLAGS+=" -Wno-discarded-qualifiers" diff --git a/2.7.8 b/2.7.8 index f30c7ab..82cf94a 100644 --- a/2.7.8 +++ b/2.7.8 @@ -3,7 +3,15 @@ install_bundler() { "$PREFIX_PATH"/bin/gem install bundler:2.4.22 } -RUBY_CFLAGS="-O3 -march=native" +# -march=native was here and has been dropped: measured ~12% *slower* than plain -O3 on +# Zen 4, worst on numeric loops (-16% integer, -21% float). Microbenchmarks are the +# friendliest case for it, so there's no reason to expect a win on real work either. +# +# -fno-strict-overflow guards fixnum overflow checks that assume signed overflow wraps — +# undefined behaviour GCC exploits from -O2 up. 2.7 uses builtin overflow intrinsics and +# doesn't strictly need it, but it's free (within noise here) and keeps every definition +# in this repo on the same flags. +RUBY_CFLAGS="-O3 -fno-strict-overflow" case "$(cc -v 2>&1)" in *gcc*) RUBY_CFLAGS+=" -Wno-discarded-qualifiers" diff --git a/README.md b/README.md index 4ba30e7..333bce0 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,32 @@ macOS the OpenSSL 1.0 builds come from [basecamp/homebrew-dev](https://github.co everywhere else OpenSSL is compiled from source into the Ruby's own prefix, so nothing lands system-wide and nothing is shared between versions. +### Optimization flags + +Every definition builds with `-O3 -fno-strict-overflow`. Both halves matter: + +**`-O3`** — leaving `RUBY_CFLAGS` empty is not "use configure's default". ruby-build exports +it as `CFLAGS`, which supersedes configure's own `optflags` in the compile line, so an empty +value builds at `-O0`. That's 2–3.5× slower. Confusingly, `RbConfig::CONFIG["optflags"]` +still reports `-O3` in that case; time a build rather than believing it. + +**`-fno-strict-overflow`** — these sources predate the compilers building them, and their +fixnum overflow checks assume signed overflow wraps. That's undefined behaviour, and GCC +exploits it from `-O2` up. Build 1.8.7 at `-O3` without this flag and it compiles cleanly, +runs, loads every stdlib — and evaluates `2**64` to `0`, typed `Fixnum`. Silent wrong +arithmetic. `test/build` asserts against exactly this, so the trap can't come back. + +`-march=native` is deliberately **not** used. It measured ~12% slower than plain `-O3` on +Zen 4, worst on numeric loops (−16% integer, −21% float), and microbenchmarks are the +friendliest case it gets. It would also make binaries non-portable between machines for no +gain. + +**1.9.3 caps `_FORTIFY_SOURCE` at 2 under GCC.** Ubuntu's GCC raises it to 3 automatically +whenever optimization is on, and level 3's object-size inference trips on 1.9.3 — the build +aborts with `*** buffer overflow detected ***`. This isn't a hardening regression: fortify +does nothing without optimization, so while these were building at `-O0` there was none at +all. Only 1.9.3 needs the cap. + ### Prerequisites **macOS:** Xcode command line tools and [Homebrew](https://brew.sh). diff --git a/test/build b/test/build index ec87675..79545b2 100755 --- a/test/build +++ b/test/build @@ -89,11 +89,19 @@ build_image() { } # Default post-flight checks - can be overridden via test/verify/ +# +# The arithmetic check is not paranoia. These sources predate the compilers building them +# and their fixnum overflow checks assume signed overflow wraps, which is undefined +# behaviour GCC exploits from -O2 up. A 1.8.7 built -O3 without -fno-strict-overflow +# compiles cleanly, runs, loads every library below — and evaluates 2**64 to 0. Nothing +# above this line would notice, so any change to optimization flags could ship silent wrong +# arithmetic. Kept 1.8.7-compatible: no interpolation-free heredocs, no modern syntax. default_verify_script() { cat <<'VERIFY' /opt/ruby/bin/ruby -e 'require "openssl"; puts "openssl: #{OpenSSL::OPENSSL_VERSION}"' /opt/ruby/bin/ruby -e 'require "digest/sha2"; puts "digest: ok"' /opt/ruby/bin/ruby -e 'require "zlib"; puts "zlib: ok"' +/opt/ruby/bin/ruby -e 'raise "2**64 wrong: #{2**64}" unless (2**64).to_s == "18446744073709551616"; raise "2**100 wrong" unless (2**100).to_s == "1267650600228229401496703205376"; raise "mul overflow wrong" unless (4611686018427387903 * 2).to_s == "9223372036854775806"; raise "negative overflow wrong" unless (-2**64).to_s == "-18446744073709551616"; raise "10**20 wrong" unless (10**20).to_s == "100000000000000000000"; puts "arithmetic: ok"' VERIFY } @@ -114,10 +122,19 @@ test_ruby() { verify_script=$(default_verify_script) fi - # Build Ruby and run post-flight checks + # Build Ruby and run post-flight checks. + # + # On failure, dump ruby-build's own log before exiting. Several definitions send the + # compiler output there (>&4) rather than to stdout, and the container is --rm, so + # without this the log dies with it and all you get is "BUILD FAILED" over a screen of + # curl progress bars. local build_script=" set -e - ruby-build $version /opt/ruby + ruby-build $version /opt/ruby || { + echo '--- ruby-build log (tail) ---' + tail -60 /tmp/ruby-build.*.log 2>/dev/null + exit 1 + } /opt/ruby/bin/ruby --version $verify_script " @@ -135,8 +152,12 @@ test_ruby() { printf ' %-14s %-14s ✗ %-62s %4ds\n' "$platform" "$version" "FAILED" "$elapsed" echo "fail" > "$RESULTS/$platform.$version.status" # Keep the log for the end-of-run report rather than interleaving it with - # other jobs' output as it happens. - echo "$output" | tail -30 > "$RESULTS/$platform.$version.log" + # other jobs' output as it happens. Strip curl's progress meter first — it's + # carriage-return spam that otherwise fills the tail and buries the actual error. + echo "$output" \ + | grep -vE '^ *[0-9 ]+ +[0-9]+ +[0-9]+[0-9k. ]*(--:--:--|[0-9]+:[0-9]+:[0-9]+)' \ + | grep -vE '^ *% Total|^ *Dload' \ + | tail -50 > "$RESULTS/$platform.$version.log" fi return 0 }