Commit c9aa42db82 for openssl.org

commit c9aa42db82c3cf436e1e80481e7fb7eb345fb051
Author: Bob Beck <beck@openssl.org>
Date:   Mon Aug 10 18:47:02 2026 -0600

    Make most generated files read-only, and write them atomically

    Generated files were left writable, so a hand edit looked like it had
    worked and was silently lost at the next build. Only the pods and the
    generated scripts were protected, by a chmod either side of the
    recipe, which cost a shell and two processes for each file.

    Route the generators that write to standard output through a helper
    that collects the output, sets the mode and renames the result into
    place. Renaming needs no permission on the file it replaces, so there
    is no unlocking step, and a generator that fails part way now leaves
    the previous file rather than a truncated one with a fresh timestamp
    that make would believe.

    The perlasm modules name their own output rather than writing to
    standard output, so they are unchanged and their output stays
    writable.

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Tue Sep  1 14:03:12 2026
    Merged-from: https://github.com/openssl/openssl/pull/32243

diff --git a/Configurations/unix-Makefile.tmpl b/Configurations/unix-Makefile.tmpl
index 6f0c8a602c..f8a5b1612e 100644
--- a/Configurations/unix-Makefile.tmpl
+++ b/Configurations/unix-Makefile.tmpl
@@ -1602,6 +1602,18 @@ EOF
       my $defs = join("", map { " -D".$_ } @{$args{defs}});
       my $deps = join(" ", compute_platform_depends(@{$args{generator_deps}},
                                                     @{$args{deps}}));
+      my $tofile = "\$(PERL) \$(SRCDIR)/util/file-from-stdout.pl";
+      # A generator named for more than one target cannot be producing them
+      # on standard output; it writes them itself, so its output must not be
+      # captured.
+      our %gen_targets;
+      unless (%gen_targets) {
+          foreach my $t (keys %{$unified_info{generate}}) {
+              $gen_targets{join(" ", @{$unified_info{generate}->{$t}})}++;
+          }
+      }
+      my $writes_own_files =
+          $gen_targets{join(" ", @{$args{generator}})} > 1;

       if ($args{src} =~ /\.html$/) {
           #
@@ -1651,7 +1663,8 @@ EOF
           my $ord_name = $args{generator}->[1] || $args{product};
           return <<"EOF";
 $target: $gen0 $deps \$(SRCDIR)/util/mkdef.pl
-	\$(PERL) \$(SRCDIR)/util/mkdef.pl$ord_ver --type $args{intent} --ordinals $gen0  --name $ord_name --OS $mkdef_os > $target
+	$tofile $target \\
+		\$(PERL) \$(SRCDIR)/util/mkdef.pl$ord_ver --type $args{intent} --ordinals $gen0  --name $ord_name --OS $mkdef_os
 EOF
       } elsif (platform->isasm($args{src})
                || platform->iscppasm($args{src})) {
@@ -1666,11 +1679,15 @@ EOF
           } -> {$args{intent}};

           my $generator;
+          # The perlasm modules take the output name as their last argument;
+          # m4 writes to standard output.
+          my $to_stdout = 0;
           if ($gen0 =~ /\.pl$/) {
               $generator = 'CC="$(CC)" $(PERL)'.$gen_incs.' '.$gen0.$gen_args
                   .' "$(PERLASM_SCHEME)"'.$incs.' '.$cppflags.$defs.' $(PROCESSOR)';
           } elsif ($gen0 =~ /\.m4$/) {
-              $generator = 'm4 -B 8192'.$gen_incs.' '.$gen0.$gen_args.' >'
+              $generator = 'm4 -B 8192'.$gen_incs.' '.$gen0.$gen_args;
+              $to_stdout = 1;
           } elsif ($gen0 =~ /\.S$/) {
               $generator = undef;
           } else {
@@ -1678,15 +1695,17 @@ EOF
           }

           if (defined($generator)) {
+              my $recipe = $to_stdout ? "$tofile \$\@ $generator"
+                                      : "$generator \$\@";
               return <<"EOF";
 $args{src}: $gen0 $deps
-	$generator \$@
+	$recipe
 EOF
           }
           return <<"EOF";
 $args{src}: $gen0 $deps
 	\$(CC) $incs $cppflags $defs -E $gen0 | \\
-	\$(PERL) -ne '/^#(line)?\\s*[0-9]+/ or print' > \$@
+	$tofile \$@ \$(PERL) -ne '/^#(line)?\\s*[0-9]+/ or print'
 EOF
       } elsif ($gen0 =~ m|^.*\.in$|) {
           #
@@ -1735,9 +1754,8 @@ EOF

           return <<"EOF";
 $args{src}: $gen0 $deps
-	if [ -r "\$@" ]; then chmod u+w \$@; fi
-	\$(PERL)$perlmodules "$dofile" "-o$target{build_file}" $gen0$gen_args > \$@
-	chmod a-w \$@
+	$tofile \$@ \\
+		\$(PERL)$perlmodules "$dofile" "-o$target{build_file}" $gen0$gen_args
 EOF
       } elsif (grep { $_ eq $gen0 } @{$unified_info{programs}}) {
           #
@@ -1750,7 +1768,9 @@ EOF
           # Use $(PERL) to execute wrap.pl directly to avoid calling env
           return <<"EOF";
 $args{src}: $gen0 $deps \$(BLDDIR)/util/wrap.pl
-	\$(PERL) \$(BLDDIR)/util/wrap.pl $gen0$gen_args > \$@
+	@{[ $writes_own_files
+	    ? "\$(PERL) \$(BLDDIR)/util/wrap.pl $gen0$gen_args > \$\@"
+	    : "$tofile \$\@ \$(PERL) \$(BLDDIR)/util/wrap.pl $gen0$gen_args" ]}
 EOF
       } else {
           #
@@ -1758,7 +1778,9 @@ EOF
           #
           return <<"EOF";
 $args{src}: $gen0 $deps
-	\$(PERL)$gen_incs $gen0$gen_args > \$@
+	@{[ $writes_own_files
+	    ? "\$(PERL)$gen_incs $gen0$gen_args > \$\@"
+	    : "$tofile \$\@ \$(PERL)$gen_incs $gen0$gen_args" ]}
 EOF
       }
   }
@@ -2117,11 +2139,9 @@ EOF
                            rel2abs($config{builddir}));
       return <<"EOF";
 $script: $sources configdata.pm
-	if [ -r "$script" ]; then chmod u+w $script; fi
-	\$(RM) "$script"
-	\$(PERL) "-I\$(BLDDIR)" -Mconfigdata "$dofile" \\
-	    "-o$target{build_file}" $sources > "$script"
-	chmod a+x,a-w $script
+	\$(PERL) \$(SRCDIR)/util/file-from-stdout.pl -x "$script" \\
+	    \$(PERL) "-I\$(BLDDIR)" -Mconfigdata "$dofile" \\
+	    "-o$target{build_file}" $sources
 EOF
   }
   sub generatedir {
diff --git a/util/file-from-stdout.pl b/util/file-from-stdout.pl
new file mode 100644
index 0000000000..8a8180e229
--- /dev/null
+++ b/util/file-from-stdout.pl
@@ -0,0 +1,73 @@
+#! /usr/bin/env perl
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License").  You may not use
+# this file except in compliance with the License.  You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use strict;
+use warnings;
+
+# Run a command and make its standard output into a file, in place of the
+# shell's ">".  The output is collected in a temporary file and renamed over
+# the target once the command has succeeded, so a failing command leaves the
+# previous target rather than a truncated one with a fresh timestamp.
+#
+# The target is left read-only.  Renaming onto a read-only file needs no
+# permission on the file itself, so it can still be replaced.
+#
+# Usage: file-from-stdout.pl [-x] FILE command [args...]
+
+my $executable = 0;
+
+if (@ARGV && $ARGV[0] eq "-x") {
+    $executable = 1;
+    shift @ARGV;
+}
+
+my $target = shift @ARGV;
+
+die "Usage: $0 [-x] FILE command [args...]\n"
+    unless defined $target && @ARGV;
+
+my $temp = "$target.tmp$$";
+
+unlink $temp;
+
+open my $out, ">", $temp
+    or die "Can't write $temp, $!\n";
+
+END {
+    unlink $temp if defined $temp && -e $temp;
+}
+
+open my $saved, ">&", \*STDOUT
+    or die "Can't save stdout, $!\n";
+open STDOUT, ">&", $out
+    or die "Can't redirect stdout to $temp, $!\n";
+
+my $status = system @ARGV;
+my $why = $!;
+
+open STDOUT, ">&", $saved
+    or die "Can't restore stdout, $!\n";
+close $saved;
+close $out
+    or die "Can't finish writing $temp, $!\n";
+
+if ($status != 0) {
+    my $how = $status == -1 ? "could not be run, $why"
+            : $status & 127 ? "died with signal " . ($status & 127)
+            :                 "exited with " . ($status >> 8);
+    my $code = $status == -1 || ($status & 127) ? 1 : $status >> 8;
+
+    print STDERR "$ARGV[0] $how\n";
+    exit $code;
+}
+
+chmod $executable ? 0555 : 0444, $temp
+    or die "Can't set the mode of $temp, $!\n";
+
+rename $temp, $target
+    or die "Can't rename $temp to $target, $!\n";
diff --git a/util/mkpod2html.pl b/util/mkpod2html.pl
index 0401dc7ced..15bd837805 100755
--- a/util/mkpod2html.pl
+++ b/util/mkpod2html.pl
@@ -70,6 +70,8 @@ sub format_page
     $pod = rel2abs($pod) or die "Can't convert to real path: $!";
     $out = rel2abs($out) or die "Can't convert to real path: $!";

+    unlink $out;
+
     pod2html "--infile=$pod",
              "--outfile=$out",
              "--title=$title",
@@ -90,6 +92,9 @@ sub format_page
         or die "Can't write $out, $!";
     print $fh $contents;
     close $fh;
+
+    chmod 0444, $out
+        or die "Can't set the mode of $out, $!\n";
 }

 # One named page: the output file and title are given.
diff --git a/util/mkpod2man.pl b/util/mkpod2man.pl
index 08903e4755..778dc2b091 100644
--- a/util/mkpod2man.pl
+++ b/util/mkpod2man.pl
@@ -75,12 +75,17 @@ sub format_page
     # The page is current when it is newer than the pod it comes from.
     return if -e $out && -M $out < -M $pod;

+    unlink $out;
+
     Pod::Man->new(name => uc $name,
                   section => "$section$opt_m",
                   center => "OpenSSL",
                   date => $opt_d,
                   release => $opt_r)
         ->parse_from_file($pod, $out);
+
+    chmod 0444, $out
+        or die "Can't set the mode of $out, $!\n";
 }

 my @pods = @ARGV;