Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion 1.8.7-p374
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 19 additions & 1 deletion 1.9.3-p551
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
11 changes: 10 additions & 1 deletion 2.3.3
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 10 additions & 1 deletion 2.3.8
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion 2.5.9
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion 2.7.8
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
29 changes: 25 additions & 4 deletions test/build
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,19 @@ build_image() {
}

# Default post-flight checks - can be overridden via test/verify/<version>
#
# 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
}

Expand All @@ -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
"
Expand All @@ -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
}
Expand Down
Loading