Skip to content

Commit fb80a75

Browse files
authored
Build with -O3 -fno-strict-overflow; drop -march=native (#8)
Steelmanning -march=native turned up the opposite of what we assumed, plus a bigger problem underneath it. -march=native is slower. ~12% behind plain -O3 on Zen 4, worst on the numeric loops it should help most: -16% integer, -21% float. Two runs, 21 interleaved passes, best-of. Microbenchmarks are the friendliest case it gets, so there's no reason to expect a win on real work. Dropped. The bigger find: the other four definitions were building unoptimized. Leaving RUBY_CFLAGS empty doesn't mean "use configure's default" — ruby-build exports it as CFLAGS, which supersedes configure's optflags in the compile line, so an empty value is -O0. Measured 2.2-3.5x slower across method calls, integer math, string building, hash churn and array ops. rbconfig still reports optflags: -O3 in that state, which is why it went unnoticed; the timings are what to trust. Unintended fallout from b719789, which made RUBY_CFLAGS="" unconditional where it had previously only been set under clang. Turning optimization on activates two things -O0 was silently masking, and neither is caught by "does it compile and run": -fno-strict-overflow is mandatory, not caution. These sources assume signed overflow wraps; GCC treats that as UB and exploits it from -O2 up. 1.8.7 built at -O3 without it compiles clean, runs, loads every stdlib — and evaluates 2**64 to 0, typed Fixnum. Silent wrong arithmetic. Confirmed -O2 breaks it too, and that the flag costs nothing measurable (within noise on 2.7.8, two of five benchmarks nominally faster with it). 1.9.3 additionally caps _FORTIFY_SOURCE at 2 under GCC. Ubuntu's GCC raises it to 3 whenever optimizing, and level 3's object-size inference aborts the build with "*** buffer overflow detected ***" while running the freshly built miniruby. Not a hardening regression: fortify does nothing without optimization, so at -O0 there was none at all. Scoped to 1.9.3; the rest build fine at 3. So test/build now asserts overflow arithmetic. The existing checks would have passed a Ruby computing 2**64 == 0 — verified by building the unsafe variant and watching it clear ruby --version, openssl, digest and zlib before the new assertion caught it. Failures also dump ruby-build's own log now, and strip curl's progress meter: several definitions send compiler output to that log rather than stdout, and diagnosing the 1.9.3 abort meant reproducing it by hand because the container took the log with it. Full matrix green, 12/12 on Arch and Ubuntu Noble.
1 parent e3be761 commit fb80a75

8 files changed

Lines changed: 117 additions & 10 deletions

File tree

‎1.8.7-p374‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,15 @@ install_bundler() {
3838
"$PREFIX_PATH"/bin/gem install bundler -v 1.17.3
3939
}
4040

41-
RUBY_CFLAGS=""
41+
# -O3 because leaving this empty does not mean "use configure's default". ruby-build
42+
# exports it as CFLAGS, which supersedes configure's own optflags in the compile line, so
43+
# an empty value builds at -O0 — measured 2-3x slower than the same source at -O3.
44+
#
45+
# -fno-strict-overflow is mandatory here, not belt-and-braces. 1.8.7's fixnum overflow
46+
# checks assume signed overflow wraps, which is undefined behaviour that GCC exploits from
47+
# -O2 upward: without this flag a -O3 build silently evaluates 2**64 to 0 and types it
48+
# Fixnum. Wrong arithmetic, no warning, no crash. It costs nothing measurable.
49+
RUBY_CFLAGS="-O3 -fno-strict-overflow"
4250
case "$(cc -v 2>&1)" in
4351
*gcc*)
4452
RUBY_CFLAGS+=" -Wno-discarded-qualifiers"

‎1.9.3-p551‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ install_bundler() {
1919
"$PREFIX_PATH"/bin/gem install bundler -v 1.17.3
2020
}
2121

22-
RUBY_CFLAGS=""
22+
# -O3 because leaving this empty does not mean "use configure's default". ruby-build
23+
# exports it as CFLAGS, which supersedes configure's own optflags, so an empty value builds
24+
# at -O0 — measured 2-3x slower than the same source at -O3.
25+
#
26+
# -fno-strict-overflow guards the fixnum overflow checks in these old sources, which assume
27+
# signed overflow wraps — undefined behaviour that GCC exploits from -O2 up. On 1.8.7 its
28+
# absence silently makes 2**64 evaluate to 0. Kept uniform across every definition here;
29+
# it costs nothing measurable.
30+
RUBY_CFLAGS="-O3 -fno-strict-overflow"
2331
case "$(cc -v 2>&1)" in
2432
*gcc*)
2533
RUBY_CFLAGS+=" -Wno-discarded-qualifiers"
@@ -28,6 +36,16 @@ case "$(cc -v 2>&1)" in
2836
RUBY_CFLAGS+=" -Wno-return-type"
2937
# GCC 15 porting: Use -std=gnu99 with relaxed type checking
3038
RUBY_CFLAGS+=" -std=gnu99 -Wno-error=implicit-function-declaration"
39+
# Cap _FORTIFY_SOURCE at 2. Ubuntu's GCC defines it to 3 automatically whenever
40+
# optimization is on, and level 3's more aggressive object-size inference trips on
41+
# 1.9.3: the build aborts with "*** buffer overflow detected ***" while running the
42+
# freshly built miniruby to generate exts.mk. Level 2 builds and runs clean.
43+
#
44+
# This is not a loss of hardening relative to before. _FORTIFY_SOURCE does nothing
45+
# without optimization, so while this definition was (unintentionally) building at
46+
# -O0, no fortification was happening at all. Only 1.9.3 needs the cap; the other
47+
# definitions build fine at level 3.
48+
RUBY_CFLAGS+=" -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2"
3149
;;
3250
*clang*)
3351
RUBY_CFLAGS+=" -Wno-compound-token-split-by-macro"

‎2.3.3‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ install_bundler() {
1212
"$PREFIX_PATH"/bin/gem install bundler:1.17.3
1313
}
1414

15-
RUBY_CFLAGS=""
15+
# -O3 because leaving this empty does not mean "use configure's default". ruby-build
16+
# exports it as CFLAGS, which supersedes configure's own optflags, so an empty value builds
17+
# at -O0 — measured 2-3x slower than the same source at -O3. (rbconfig still reports
18+
# optflags: -O3 in that case, which is misleading; the timings are what to trust.)
19+
#
20+
# -fno-strict-overflow guards the fixnum overflow checks in these old sources, which assume
21+
# signed overflow wraps — undefined behaviour that GCC exploits from -O2 up. On 1.8.7 its
22+
# absence silently makes 2**64 evaluate to 0. Kept uniform across every definition here;
23+
# it costs nothing measurable.
24+
RUBY_CFLAGS="-O3 -fno-strict-overflow"
1625
case "$(cc -v 2>&1)" in
1726
*gcc*)
1827
RUBY_CFLAGS+=" -Wno-discarded-qualifiers"

‎2.3.8‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ install_bundler() {
1212
"$PREFIX_PATH"/bin/gem install bundler:1.17.3
1313
}
1414

15-
RUBY_CFLAGS=""
15+
# -O3 because leaving this empty does not mean "use configure's default". ruby-build
16+
# exports it as CFLAGS, which supersedes configure's own optflags, so an empty value builds
17+
# at -O0 — measured 2.6-3.5x slower than the same source at -O3. (rbconfig still reports
18+
# optflags: -O3 in that case, which is misleading; the timings are what to trust.)
19+
#
20+
# -fno-strict-overflow guards the fixnum overflow checks in these old sources, which assume
21+
# signed overflow wraps — undefined behaviour that GCC exploits from -O2 up. On 1.8.7 its
22+
# absence silently makes 2**64 evaluate to 0. Kept uniform across every definition here;
23+
# it costs nothing measurable.
24+
RUBY_CFLAGS="-O3 -fno-strict-overflow"
1625
case "$(cc -v 2>&1)" in
1726
*gcc*)
1827
RUBY_CFLAGS+=" -Wno-discarded-qualifiers"

‎2.5.9‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ install_bundler() {
33
"$PREFIX_PATH"/bin/gem install bundler:2.3.27
44
}
55

6-
RUBY_CFLAGS="-O3 -march=native"
6+
# -march=native was here and has been dropped: measured ~12% *slower* than plain -O3 on
7+
# Zen 4, worst on numeric loops (-16% integer, -21% float). Microbenchmarks are the
8+
# friendliest case for it, so there's no reason to expect a win on real work either.
9+
#
10+
# -fno-strict-overflow guards fixnum overflow checks that assume signed overflow wraps —
11+
# undefined behaviour GCC exploits from -O2 up. 2.5 uses builtin overflow intrinsics and
12+
# doesn't strictly need it, but it's free (within noise here) and keeps every definition
13+
# in this repo on the same flags.
14+
RUBY_CFLAGS="-O3 -fno-strict-overflow"
715
case "$(cc -v 2>&1)" in
816
*gcc*)
917
RUBY_CFLAGS+=" -Wno-discarded-qualifiers"

‎2.7.8‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ install_bundler() {
33
"$PREFIX_PATH"/bin/gem install bundler:2.4.22
44
}
55

6-
RUBY_CFLAGS="-O3 -march=native"
6+
# -march=native was here and has been dropped: measured ~12% *slower* than plain -O3 on
7+
# Zen 4, worst on numeric loops (-16% integer, -21% float). Microbenchmarks are the
8+
# friendliest case for it, so there's no reason to expect a win on real work either.
9+
#
10+
# -fno-strict-overflow guards fixnum overflow checks that assume signed overflow wraps —
11+
# undefined behaviour GCC exploits from -O2 up. 2.7 uses builtin overflow intrinsics and
12+
# doesn't strictly need it, but it's free (within noise here) and keeps every definition
13+
# in this repo on the same flags.
14+
RUBY_CFLAGS="-O3 -fno-strict-overflow"
715
case "$(cc -v 2>&1)" in
816
*gcc*)
917
RUBY_CFLAGS+=" -Wno-discarded-qualifiers"

‎README.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,32 @@ macOS the OpenSSL 1.0 builds come from [basecamp/homebrew-dev](https://github.co
2121
everywhere else OpenSSL is compiled from source into the Ruby's own prefix, so nothing
2222
lands system-wide and nothing is shared between versions.
2323

24+
### Optimization flags
25+
26+
Every definition builds with `-O3 -fno-strict-overflow`. Both halves matter:
27+
28+
**`-O3`** — leaving `RUBY_CFLAGS` empty is not "use configure's default". ruby-build exports
29+
it as `CFLAGS`, which supersedes configure's own `optflags` in the compile line, so an empty
30+
value builds at `-O0`. That's 2–3.5× slower. Confusingly, `RbConfig::CONFIG["optflags"]`
31+
still reports `-O3` in that case; time a build rather than believing it.
32+
33+
**`-fno-strict-overflow`** — these sources predate the compilers building them, and their
34+
fixnum overflow checks assume signed overflow wraps. That's undefined behaviour, and GCC
35+
exploits it from `-O2` up. Build 1.8.7 at `-O3` without this flag and it compiles cleanly,
36+
runs, loads every stdlib — and evaluates `2**64` to `0`, typed `Fixnum`. Silent wrong
37+
arithmetic. `test/build` asserts against exactly this, so the trap can't come back.
38+
39+
`-march=native` is deliberately **not** used. It measured ~12% slower than plain `-O3` on
40+
Zen 4, worst on numeric loops (−16% integer, −21% float), and microbenchmarks are the
41+
friendliest case it gets. It would also make binaries non-portable between machines for no
42+
gain.
43+
44+
**1.9.3 caps `_FORTIFY_SOURCE` at 2 under GCC.** Ubuntu's GCC raises it to 3 automatically
45+
whenever optimization is on, and level 3's object-size inference trips on 1.9.3 — the build
46+
aborts with `*** buffer overflow detected ***`. This isn't a hardening regression: fortify
47+
does nothing without optimization, so while these were building at `-O0` there was none at
48+
all. Only 1.9.3 needs the cap.
49+
2450
### Prerequisites
2551

2652
**macOS:** Xcode command line tools and [Homebrew](https://brew.sh).

‎test/build‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,19 @@ build_image() {
8989
}
9090

9191
# Default post-flight checks - can be overridden via test/verify/<version>
92+
#
93+
# The arithmetic check is not paranoia. These sources predate the compilers building them
94+
# and their fixnum overflow checks assume signed overflow wraps, which is undefined
95+
# behaviour GCC exploits from -O2 up. A 1.8.7 built -O3 without -fno-strict-overflow
96+
# compiles cleanly, runs, loads every library below — and evaluates 2**64 to 0. Nothing
97+
# above this line would notice, so any change to optimization flags could ship silent wrong
98+
# arithmetic. Kept 1.8.7-compatible: no interpolation-free heredocs, no modern syntax.
9299
default_verify_script() {
93100
cat <<'VERIFY'
94101
/opt/ruby/bin/ruby -e 'require "openssl"; puts "openssl: #{OpenSSL::OPENSSL_VERSION}"'
95102
/opt/ruby/bin/ruby -e 'require "digest/sha2"; puts "digest: ok"'
96103
/opt/ruby/bin/ruby -e 'require "zlib"; puts "zlib: ok"'
104+
/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"'
97105
VERIFY
98106
}
99107

@@ -114,10 +122,19 @@ test_ruby() {
114122
verify_script=$(default_verify_script)
115123
fi
116124

117-
# Build Ruby and run post-flight checks
125+
# Build Ruby and run post-flight checks.
126+
#
127+
# On failure, dump ruby-build's own log before exiting. Several definitions send the
128+
# compiler output there (>&4) rather than to stdout, and the container is --rm, so
129+
# without this the log dies with it and all you get is "BUILD FAILED" over a screen of
130+
# curl progress bars.
118131
local build_script="
119132
set -e
120-
ruby-build $version /opt/ruby
133+
ruby-build $version /opt/ruby || {
134+
echo '--- ruby-build log (tail) ---'
135+
tail -60 /tmp/ruby-build.*.log 2>/dev/null
136+
exit 1
137+
}
121138
/opt/ruby/bin/ruby --version
122139
$verify_script
123140
"
@@ -135,8 +152,12 @@ test_ruby() {
135152
printf ' %-14s %-14s ✗ %-62s %4ds\n' "$platform" "$version" "FAILED" "$elapsed"
136153
echo "fail" > "$RESULTS/$platform.$version.status"
137154
# Keep the log for the end-of-run report rather than interleaving it with
138-
# other jobs' output as it happens.
139-
echo "$output" | tail -30 > "$RESULTS/$platform.$version.log"
155+
# other jobs' output as it happens. Strip curl's progress meter first — it's
156+
# carriage-return spam that otherwise fills the tail and buries the actual error.
157+
echo "$output" \
158+
| grep -vE '^ *[0-9 ]+ +[0-9]+ +[0-9]+[0-9k. ]*(--:--:--|[0-9]+:[0-9]+:[0-9]+)' \
159+
| grep -vE '^ *% Total|^ *Dload' \
160+
| tail -50 > "$RESULTS/$platform.$version.log"
140161
fi
141162
return 0
142163
}

0 commit comments

Comments
 (0)