#!/usr/bin/env perl # # apply-cr2-race-fix.pl -- NetBSD 11.0/i386: read CR2 before enabling # interrupts on a page fault. # # The bug: the i386 page-fault vector, IDTVEC(trap0e) in # sys/arch/i386/i386/i386_trap.S, enables interrupts before trap() reads # CR2. The CPU rewrites CR2 on every page fault, so a fault taken in that # window, by an interrupt handler or a preempted LWP demand-paging, # overwrites it. uvm_fault() then runs on the wrong address, and a copy # that would have succeeded returns EFAULT. On a 486 InterJet: 33 faults # in 500 iterations without the fix, 0 in 500 with it. # # The fix, compiled in only with "options CR2_RACE_FIX": # 1. i386_trap.S: no STI in trap0e, so interrupts stay off into trap(). # 2. trap.c: x86_enable_intr() right after each of the two # "cr2 = rcr2()" reads, before anything can sleep. # The early returns before the read leave through iret, which restores the # interrupt flag from the trap frame. Saving CR2 in the stub would be the # cleaner fix, but the i386 trap frame has no slot for it. # # CR2_RACE_FIX is not declared to config(1): a kernel built from an # unpatched tree with the option set builds, and is not fixed. # # Edits the tree in place. Idempotent; --revert undoes it. It refuses # any path that does not end in .../src./usr/src, so that a shared # tree is never patched by mistake. # # Usage: apply-cr2-race-fix.pl [--revert] # # Author: Royce Williams # SPDX-License-Identifier: MIT # # Copyright (c) 2026 Royce Williams # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. use strict; use warnings; if (! @ARGV) { print STDERR <<'USAGE'; usage: apply-cr2-race-fix.pl [--revert] Patch i386_trap.S and trap.c so "options CR2_RACE_FIX" reads CR2 before enabling interrupts on a page fault. Idempotent. Refuses a shared/pristine tree. --revert removes it. USAGE exit 2; } my ($tree, $revert) = (undef, 0); for my $a (@ARGV) { if ($a eq '--revert') { $revert = 1 } elsif ($a =~ /^-/) { die "unknown argument $a\n" } else { $tree = $a } } defined $tree or die "a source tree path is required\n"; $tree =~ m{/src\.[^/]+/usr/src/?$} or die "refusing '$tree': not a private tree (expected .../src./usr/src)\n"; my $S_OLD = <<'OLD'; INTRENTRY STI(%eax) testb $PGEX_U,TF_ERR(%esp) OLD my $S_NEW = <<'NEW'; INTRENTRY #ifndef CR2_RACE_FIX STI(%eax) #endif testb $PGEX_U,TF_ERR(%esp) NEW my $C_OLD = "\t\tcr2 = rcr2();\n"; my $C_NEW = "\t\tcr2 = rcr2();\n" . "#ifdef CR2_RACE_FIX\n" . "\t\t/* CR2 is now safely in hand; let interrupts back in\n" . "\t\t * before anything that can sleep. */\n" . "\t\tx86_enable_intr();\n" . "#endif\n"; my @edits = ( { file => "$tree/sys/arch/i386/i386/i386_trap.S", old => $S_OLD, new => $S_NEW, n => 1 }, { file => "$tree/sys/arch/i386/i386/trap.c", old => $C_OLD, new => $C_NEW, n => 2 }, ); for my $e (@edits) { my $file = $e->{file}; -f $file or die "no such file: $file\n"; open(my $in, '<:raw', $file) or die "cannot read $file: $!\n"; my $text = do { local $/; <$in> }; close $in; my $done = () = $text =~ /\QCR2_RACE_FIX\E/g; if ($revert) { $done or die "$file: not applied; nothing to revert\n"; my $n = ($text =~ s/\Q$e->{new}\E/$e->{old}/g); $n == $e->{n} or die "$file: revert changed $n sites, expected $e->{n}\n"; } else { if ($done) { print STDERR "$file: already applied\n"; next } my $c = () = $text =~ /\Q$e->{old}\E/g; $c == $e->{n} or die "$file: anchor found $c times, expected $e->{n}.\n" . "Refusing: this is not the 11.0 source this was written for.\n"; $text =~ s/\Q$e->{old}\E/$e->{new}/g; } my $mode = (stat $file)[2] & 07777; my $tmp = "$file.new.$$"; open(my $out, '>:raw', $tmp) or die "cannot write $tmp: $!\n"; print $out $text or die "write failed: $!\n"; close $out or die "close failed: $!\n"; chmod $mode, $tmp or die "chmod failed: $!\n"; rename $tmp, $file or die "rename failed: $!\n"; open(my $chk, '<:raw', $file) or die "cannot re-read $file: $!\n"; my $after = do { local $/; <$chk> }; close $chk; my $now = () = $after =~ /\QCR2_RACE_FIX\E/g; my $want = $revert ? 0 : $e->{n}; $now == $want or die "$file: post-check found $now, expected $want\n"; printf STDERR "apply-cr2-race-fix.pl: %s %s (%d site(s), mode %04o)\n", ($revert ? 'reverted' : 'applied'), $file, $want, $mode; }