Commit d4357db45c for openssl.org
commit d4357db45c936389da75fba3a95ed98e711865bd
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Mon Jul 13 18:49:29 2026 +0200
mkwraps: resolve system (libc) functions via compiler include paths
WRAP[] lists mix OpenSSL functions with libc/POSIX ones such as read()
or socket(), which mkwraps.pl reported as "declaration not found" since
it only searched the project's INCLUDE[] directories.
Fall back to the C compiler's default include search paths for functions
missing from the project headers. They are queried lazily, only on such
a miss, so the all-OpenSSL case still does not walk /usr/include. Parse
the glibc trailing attribute macros (__THROW, __wur, ...) and restrict
qualifier, and add --cc and --no-system.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Reviewed-by: Paul Dale <paul.dale@oracle.com>
MergeDate: Mon Jul 20 07:00:57 2026
(Merged from https://github.com/openssl/openssl/pull/31936)
diff --git a/test/unit/README.md b/test/unit/README.md
index 42fd1873d5..b945b438ab 100644
--- a/test/unit/README.md
+++ b/test/unit/README.md
@@ -513,7 +513,9 @@ list is tedious and error-prone, so the helper script `util/mkwraps.pl`
generates a first draft from the `build.info` declaration. It reads the
`WRAP[<target>]` list, searches the headers under the target's `INCLUDE[]`
directories for each function's prototype, and emits matching wrap functions
-and expectation helpers.
+and expectation helpers. Functions not found there (typically libc/POSIX
+functions such as `read` or `socket`) are looked up under the compiler's
+default system include paths, and emitted with angle-bracket includes.
```console
$ ./util/mkwraps.pl --build-info test/unit/build.info \
@@ -526,6 +528,10 @@ Useful options:
`expect_*` helpers, or both (the default).
* `--include DIR`: add an extra header search directory beyond those in
`INCLUDE[]`. Cumulative.
+ * `--cc NAME`: C compiler queried for the system include paths (default
+ `$CC` or `cc`).
+ * `--no-system`: do not fall back to the compiler's system include
+ directories for functions missing from the project headers.
* `--output FILE`: write to a file instead of standard output.
* `--verbose`: report progress and where each prototype was found.
diff --git a/util/mkwraps.pl b/util/mkwraps.pl
index d7e8bcb7db..91a44cbb3f 100755
--- a/util/mkwraps.pl
+++ b/util/mkwraps.pl
@@ -18,6 +18,8 @@ my $target;
my @extra_includes;
my $output_file;
my $mode = 'both';
+my $cc;
+my $use_system = 1;
my $verbose = 0;
my $help = 0;
@@ -26,10 +28,14 @@ GetOptions('build-info=s' => \$build_info_file,
'include=s' => \@extra_includes,
'output=s' => \$output_file,
'mode=s' => \$mode,
+ 'cc=s' => \$cc,
+ 'system!' => \$use_system,
'verbose' => \$verbose,
'help' => \$help)
or die "Error in command line arguments\n";
+$cc = $ENV{CC} || 'cc' unless defined $cc;
+
sub help
{
print STDERR <<"EOF";
@@ -48,6 +54,14 @@ Options:
--mode MODE What to emit: 'wraps', 'expects' or 'both'.
Defaults to 'both'.
+ --cc NAME C compiler used to discover the default system
+ include search paths. Defaults to \$CC or 'cc'.
+
+ --no-system Do not fall back to the compiler's default system
+ include directories. By default, functions not
+ found in the project headers (typically libc/POSIX
+ functions) are looked up there.
+
--verbose Print progress to stderr.
--help Show this help text.
@@ -60,6 +74,12 @@ mirroring how the compiler resolves <subdir/header.h> via -I flags.
The first header containing a matching declaration provides the
prototype.
+Functions not found in those directories (typically system functions
+such as read() or socket()) are then looked up under the compiler's
+default include search paths, as reported by the C compiler. System
+prototypes are emitted with angle-bracket #include directives. Use
+--no-system to disable this fallback.
+
The output is meant as a stub for further editing. Custom logic
(out-parameters, variadic forwarding, side effects on globals) still
needs to be written manually, and the emitted #include directives may
@@ -120,18 +140,20 @@ print STDERR "Search dirs:\n ", join("\n ", @search_dirs), "\n" if $verbose;
# Walk all search directories and get them in the order that level ones are
# first followed by subdirs so if there are nested includes, we get the
-# shortest ones first searched.
+# shortest ones first searched. Each base is a [dir, is_system] pair and the
+# is_system flag is carried onto every header found beneath it, so that system
+# prototypes can later be emitted with angle-bracket includes.
sub find_headers
{
my (@bases) = @_;
my @found;
- my @queue = map { [$_, ''] } @bases;
+ my @queue = map { [$_->[0], '', $_->[1]] } @bases;
while (@queue) {
my @next;
my @level_files;
foreach my $entry (@queue) {
- my ($dir, $rel) = @$entry;
+ my ($dir, $rel, $sys) = @$entry;
next unless -d $dir;
opendir(my $dh, $dir) or next;
foreach my $name (readdir $dh) {
@@ -139,9 +161,9 @@ sub find_headers
my $full = catfile($dir, $name);
my $newrel = $rel eq '' ? $name : "$rel/$name";
if (-d $full) {
- push @next, [$full, $newrel];
+ push @next, [$full, $newrel, $sys];
} elsif (-f $full && $name =~ /\.h$/) {
- push @level_files, [$full, $newrel];
+ push @level_files, [$full, $newrel, $sys];
}
}
closedir $dh;
@@ -153,7 +175,45 @@ sub find_headers
return @found;
}
-my @search_files = find_headers(@search_dirs);
+# Ask the C compiler for its default "#include <...>" search paths. Returns
+# the list of existing directories, in search order. Empty on failure.
+sub system_include_dirs
+{
+ my ($compiler) = @_;
+ my $out = `$compiler -xc -E -v /dev/null 2>&1`;
+ return () unless defined $out
+ && $out =~ /search starts here:(.*?)End of search list\./s;
+ my @dirs;
+ foreach my $line (split /\n/, $1) {
+ $line =~ s/^\s+//;
+ $line =~ s/\s+$//;
+ # clang annotates framework directories; skip those.
+ next if $line eq '' || $line =~ /\(framework directory\)$/;
+ push @dirs, $line if -d $line;
+ }
+ return @dirs;
+}
+
+# Project headers are searched first (with is_system = 0) so that a project
+# declaration always wins over a colliding system one. The system headers are
+# appended lazily, only if some function is not found in the project headers.
+my @search_files = find_headers(map { [$_, 0] } @search_dirs);
+my $system_loaded = 0;
+
+sub load_system_headers
+{
+ return if $system_loaded;
+ $system_loaded = 1;
+ return unless $use_system;
+ my @sys_dirs = system_include_dirs($cc);
+ unless (@sys_dirs) {
+ warn "WARNING: could not determine system include dirs from '$cc'\n";
+ return;
+ }
+ print STDERR "System include dirs:\n ", join("\n ", @sys_dirs), "\n"
+ if $verbose;
+ push @search_files, find_headers(map { [$_, 1] } @sys_dirs);
+}
my %file_cache;
@@ -184,12 +244,41 @@ sub strip_attribute_macros
return $s;
}
+# Consume reserved-namespace decorations between a declaration's closing
+# parenthesis and its semicolon, e.g. glibc's __THROW, __wur or
+# __attr_access ((...)). Only __-prefixed tokens (with an optional balanced
+# argument list) are eaten, so a genuine following declaration is left alone.
+sub skip_trailing_attributes
+{
+ my $s = shift;
+ while (1) {
+ $s =~ s/^\s+//;
+ last unless $s =~ /^(__\w+)/;
+ $s = substr($s, length($1));
+ $s =~ s/^\s+//;
+ if ($s =~ /^\(/) {
+ my $depth = 0;
+ my $i = 0;
+ while ($i < length($s)) {
+ my $c = substr($s, $i, 1);
+ $depth++ if $c eq '(';
+ $depth-- if $c eq ')';
+ $i++;
+ last if $depth == 0;
+ }
+ return $s if $depth != 0;
+ $s = substr($s, $i);
+ }
+ }
+ return $s;
+}
+
sub find_function_decl
{
my ($funcname) = @_;
foreach my $entry (@search_files) {
- my ($file, $relpath) = @$entry;
+ my ($file, $relpath, $is_system) = @$entry;
unless (exists $file_cache{$file}) {
my $text = '';
if (open(my $fh, '<', $file)) {
@@ -218,9 +307,10 @@ sub find_function_decl
my $params_str =
substr($text, $paren_start, $cursor - $paren_start - 1);
- # What follows must be ; for this to be a declaration.
+ # What follows must be ; for this to be a declaration, possibly
+ # after trailing attribute macros (__THROW, __wur, ...).
my $after = substr($text, $cursor);
- $after =~ s/^\s+//;
+ $after = skip_trailing_attributes($after);
next unless $after =~ /^;/;
# Anything since the previous statement terminator is the return
@@ -239,7 +329,8 @@ sub find_function_decl
rettype => $rettype,
params => $params_str,
file => $file,
- include_path => $include_path };
+ include_path => $include_path,
+ system => $is_system };
}
}
return undef;
@@ -282,6 +373,9 @@ sub parse_param
return { type => '', name => '', is_variadic => 1, is_ptr => 0 }
if $param eq '...';
+ # Drop the restrict qualifier; it plays no role in a mock signature.
+ $param =~ s/\b(?:__restrict(?:__)?|restrict)\b//g;
+
# Reduce TYPE NAME[size] to TYPE * NAME for our purposes.
my $is_array = 0;
$is_array = 1 if $param =~ s/\[\s*[^\]]*\s*\]\s*$//;
@@ -296,6 +390,10 @@ sub parse_param
$name = '';
}
+ # System headers name parameters in the reserved __ namespace; strip the
+ # leading underscores so the generated wrap uses ordinary local names.
+ $name =~ s/^_+//;
+
return { type => $type,
name => $name,
is_ptr => (($type =~ /\*/) || $is_array) ? 1 : 0,
@@ -320,6 +418,11 @@ my @found_includes;
my %seen_include;
foreach my $func (@wraps) {
my $info = find_function_decl($func);
+ if (!defined $info && !$system_loaded) {
+ # Not in the project headers: pull in the system ones and retry.
+ load_system_headers();
+ $info = find_function_decl($func);
+ }
unless (defined $info) {
warn "WARNING: $func: declaration not found in any include dir\n";
next;
@@ -340,7 +443,8 @@ foreach my $func (@wraps) {
unless ($seen_include{$info->{include_path}}) {
$seen_include{$info->{include_path}} = 1;
- push @found_includes, $info->{include_path};
+ push @found_includes, { path => $info->{include_path},
+ system => $info->{system} };
}
print STDERR " found $func in $info->{file}\n" if $verbose;
}
@@ -369,19 +473,19 @@ EOF
print $out_fh "#include <cmocka.h>\n";
print $out_fh "\n";
if (@found_includes) {
- my @system;
+ my @angle;
my @local;
- foreach my $inc (sort @found_includes) {
- if ($inc =~ m|^openssl/|) {
- push @system, $inc;
+ foreach my $inc (sort { $a->{path} cmp $b->{path} } @found_includes) {
+ if ($inc->{system} || $inc->{path} =~ m|^openssl/|) {
+ push @angle, $inc->{path};
} else {
- push @local, $inc;
+ push @local, $inc->{path};
}
}
- foreach my $inc (@system) {
+ foreach my $inc (@angle) {
print $out_fh "#include <$inc>\n";
}
- print $out_fh "\n" if @system && @local;
+ print $out_fh "\n" if @angle && @local;
foreach my $inc (@local) {
print $out_fh "#include \"$inc\"\n";
}