Add some utilities for testing temporary files

Avoid rewriting the FILE scoper, and deal with the Android problem in
one place. This header will also, in the next CL, be the home for a
temporary directory helper for hash_dir.

Change-Id: I4be69ef6c2ac3443b80ee8852bcce4078bf7f118
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/66007
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Bob Beck <bbe@google.com>
diff --git a/crypto/bio/bio_test.cc b/crypto/bio/bio_test.cc
index c9e0ae0..7115b98 100644
--- a/crypto/bio/bio_test.cc
+++ b/crypto/bio/bio_test.cc
@@ -24,6 +24,7 @@
 #include <openssl/mem.h>
 
 #include "../internal.h"
+#include "../test/file_util.h"
 #include "../test/test_util.h"
 
 #if !defined(OPENSSL_WINDOWS)
@@ -37,6 +38,7 @@
 #include <unistd.h>
 #else
 #include <io.h>
+#include <fcntl.h>
 OPENSSL_MSVC_PRAGMA(warning(push, 3))
 #include <winsock2.h>
 #include <ws2tcpip.h>
@@ -633,50 +635,59 @@
       check_bio_gets(bio.get());
     }
 
-    struct FileCloser {
-      void operator()(FILE *f) const { fclose(f); }
-    };
-    using ScopedFILE = std::unique_ptr<FILE, FileCloser>;
-    ScopedFILE file(tmpfile());
-#if defined(OPENSSL_ANDROID)
-    // On Android, when running from an APK, |tmpfile| does not work. See
-    // b/36991167#comment8.
-    if (!file) {
-      fprintf(stderr, "tmpfile failed: %s (%d). Skipping file-based tests.\n",
-              strerror(errno), errno);
-      continue;
-    }
-#else
-    ASSERT_TRUE(file);
-#endif
+    if (!SkipTempFileTests()) {
+      TemporaryFile file;
+      ASSERT_TRUE(file.Init(t.bio));
 
-    if (!t.bio.empty()) {
-      ASSERT_EQ(1u,
-                fwrite(t.bio.data(), t.bio.size(), /*nitems=*/1, file.get()));
-      ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET));
-    }
+      // TODO(crbug.com/boringssl/585): If the line has an embedded NUL, file
+      // BIOs do not currently report the answer correctly.
+      if (t.bio.find('\0') == std::string::npos) {
+        SCOPED_TRACE("file");
 
-    // TODO(crbug.com/boringssl/585): If the line has an embedded NUL, file
-    // BIOs do not currently report the answer correctly.
-    if (t.bio.find('\0') == std::string::npos) {
-      SCOPED_TRACE("file");
-      bssl::UniquePtr<BIO> bio(BIO_new_fp(file.get(), BIO_NOCLOSE));
-      ASSERT_TRUE(bio);
-      check_bio_gets(bio.get());
-    }
+        // Test |BIO_new_file|.
+        bssl::UniquePtr<BIO> bio(BIO_new_file(file.path().c_str(), "r"));
+        ASSERT_TRUE(bio);
+        check_bio_gets(bio.get());
 
-    ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET));
+        // Test |BIO_NOCLOSE|.
+        ScopedFILE file_obj = file.Open("r");
+        ASSERT_TRUE(file_obj);
+        bio.reset(BIO_new_fp(file_obj.get(), BIO_NOCLOSE));
+        ASSERT_TRUE(bio);
+        check_bio_gets(bio.get());
 
-    {
-      SCOPED_TRACE("fd");
+        // Test |BIO_CLOSE|.
+        file_obj = file.Open("r");
+        ASSERT_TRUE(file_obj);
+        bio.reset(BIO_new_fp(file_obj.get(), BIO_CLOSE));
+        ASSERT_TRUE(bio);
+        file_obj.release();  // |BIO_new_fp| took ownership on success.
+        check_bio_gets(bio.get());
+      }
+
+      {
+        SCOPED_TRACE("fd");
 #if defined(OPENSSL_WINDOWS)
-      int fd = _fileno(file.get());
+        int open_flags = _O_RDONLY | _O_BINARY;
 #else
-      int fd = fileno(file.get());
+        int open_flags = O_RDONLY;
 #endif
-      bssl::UniquePtr<BIO> bio(BIO_new_fd(fd, BIO_NOCLOSE));
-      ASSERT_TRUE(bio);
-      check_bio_gets(bio.get());
+
+        // Test |BIO_NOCLOSE|.
+        ScopedFD fd = file.OpenFD(open_flags);
+        ASSERT_TRUE(fd.is_valid());
+        bssl::UniquePtr<BIO> bio(BIO_new_fd(fd.get(), BIO_NOCLOSE));
+        ASSERT_TRUE(bio);
+        check_bio_gets(bio.get());
+
+        // Test |BIO_CLOSE|.
+        fd = file.OpenFD(open_flags);
+        ASSERT_TRUE(fd.is_valid());
+        bio.reset(BIO_new_fd(fd.get(), BIO_CLOSE));
+        ASSERT_TRUE(bio);
+        fd.release();  // |BIO_new_fd| took ownership on success.
+        check_bio_gets(bio.get());
+      }
     }
   }
 
diff --git a/crypto/test/file_util.cc b/crypto/test/file_util.cc
new file mode 100644
index 0000000..ede0d76
--- /dev/null
+++ b/crypto/test/file_util.cc
@@ -0,0 +1,163 @@
+/* Copyright (c) 2023, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#include "file_util.h"
+
+#include <stdlib.h>
+
+#if defined(OPENSSL_WINDOWS)
+OPENSSL_MSVC_PRAGMA(warning(push, 3))
+#include <windows.h>
+OPENSSL_MSVC_PRAGMA(warning(pop))
+#else
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#endif
+
+#include <openssl/rand.h>
+
+#include "test_util.h"
+
+
+#if defined(OPENSSL_WINDOWS)
+static void PrintLastError(const char *s) {
+  DWORD error = GetLastError();
+  char *buffer;
+  DWORD len = FormatMessageA(
+      FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, 0, error, 0,
+      reinterpret_cast<char *>(&buffer), 0, nullptr);
+  std::string msg = "unknown error";
+  if (len > 0) {
+    msg.assign(buffer, len);
+    while (!msg.empty() && (msg.back() == '\r' || msg.back() == '\n')) {
+      msg.resize(msg.size() - 1);
+    }
+  }
+  LocalFree(buffer);
+  fprintf(stderr, "%s: %s (0x%lx)\n", s, msg.c_str(), error);
+}
+#endif  // OPENSSL_WINDOWS
+
+// GetTempDir returns the path to the temporary directory, or the empty string
+// on error. On success, the result will include the directory separator.
+static std::string GetTempDir() {
+#if defined(OPENSSL_WINDOWS)
+  char buf[MAX_PATH + 1];
+  DWORD len = GetTempPathA(sizeof(buf), buf);
+  return std::string(buf, len);
+#else
+  const char *tmpdir = getenv("TMPDIR");
+  if (tmpdir != nullptr && *tmpdir != '\0') {
+    std::string ret = tmpdir;
+    if (ret.back() != '/') {
+      ret.push_back('/');
+    }
+    return ret;
+  }
+#if defined(OPENSSL_ANDROID)
+  return "/data/local/tmp/";
+#else
+  return "/tmp/";
+#endif
+#endif
+}
+
+bool SkipTempFileTests() {
+#if defined(OPENSSL_ANDROID)
+  // When running in an APK context, /data/local/tmp is unreadable. Android
+  // versions before https://android-review.googlesource.com/c/1821337 do not
+  // set TMPDIR to a suitable replacement.
+  if (getenv("TMPDIR") == nullptr) {
+    static bool should_skip = [] {
+      TemporaryFile file;
+      return !file.Init();
+    }();
+    if (should_skip) {
+      fprintf(stderr, "Skipping tests with temporary files.\n");
+      return true;
+    }
+  }
+#endif
+  return false;
+}
+
+TemporaryFile::~TemporaryFile() {
+#if defined(OPENSSL_WINDOWS)
+  if (!path_.empty() && !DeleteFileA(path_.c_str())) {
+    PrintLastError("Could not delete file");
+  }
+#else
+  if (!path_.empty() && unlink(path_.c_str()) != 0) {
+    perror("Could not delete file");
+  }
+#endif
+}
+
+bool TemporaryFile::Init(bssl::Span<const uint8_t> content) {
+  std::string temp_dir = GetTempDir();
+  if (temp_dir.empty()) {
+    return false;
+  }
+
+#if defined(OPENSSL_WINDOWS)
+  char path[MAX_PATH];
+  if (GetTempFileNameA(temp_dir.c_str(), "bssl",
+                       /*uUnique=*/0, path) == 0) {
+    PrintLastError("Could not create temporary");
+    return false;
+  }
+  path_ = path;
+#else
+  std::string path = temp_dir + "bssl_tmp_file.XXXXXX";
+  // TODO(davidben): Use |path.data()| when we require C++17.
+  int fd = mkstemp(&path[0]);
+  if (fd < 0) {
+    perror("Could not create temporary file");
+    return false;
+  }
+  close(fd);
+  path_ = std::move(path);
+#endif
+
+  ScopedFILE file = Open("wb");
+  if (file == nullptr) {
+    perror("Could not open temporary file");
+    return false;
+  }
+  if (!content.empty() &&
+      fwrite(content.data(), content.size(), /*nitems=*/1, file.get()) != 1) {
+    perror("Could not write temporary file");
+    return false;
+  }
+  return true;
+}
+
+ScopedFILE TemporaryFile::Open(const char *mode) const {
+  if (path_.empty()) {
+    return nullptr;
+  }
+  return ScopedFILE(fopen(path_.c_str(), mode));
+}
+
+ScopedFD TemporaryFile::OpenFD(int flags) const {
+  if (path_.empty()) {
+    return ScopedFD();
+  }
+#if defined(OPENSSL_WINDOWS)
+  return ScopedFD(_open(path_.c_str(), flags));
+#else
+  return ScopedFD(open(path_.c_str(), flags));
+#endif
+}
diff --git a/crypto/test/file_util.h b/crypto/test/file_util.h
new file mode 100644
index 0000000..51752b4
--- /dev/null
+++ b/crypto/test/file_util.h
@@ -0,0 +1,113 @@
+/* Copyright (c) 2023, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#ifndef OPENSSL_HEADER_CRYPTO_TEST_FILE_UTIL_H
+#define OPENSSL_HEADER_CRYPTO_TEST_FILE_UTIL_H
+
+#include <stdio.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include <openssl/span.h>
+
+#if defined(OPENSSL_WINDOWS)
+#include <io.h>
+#else
+#include <unistd.h>
+#endif
+
+
+struct FileDeleter {
+  void operator()(FILE *f) const {
+    if (f != nullptr) {
+      fclose(f);
+    }
+  }
+};
+
+using ScopedFILE = std::unique_ptr<FILE, FileDeleter>;
+
+class ScopedFD {
+ public:
+  ScopedFD() = default;
+  explicit ScopedFD(int fd) : fd_(fd) {}
+  ~ScopedFD() { reset(); }
+
+  ScopedFD(ScopedFD &&other) { *this = std::move(other); }
+  ScopedFD &operator=(ScopedFD other) {
+    reset(other.release());
+    return *this;
+  }
+
+  bool is_valid() const { return fd_ >= 0; }
+  int get() const { return fd_; }
+
+  int release() { return std::exchange(fd_, -1); }
+  void reset(int fd = -1) {
+    if (is_valid()) {
+#if defined(OPENSSL_WINDOWS)
+      _close(fd_);
+#else
+      close(fd_);
+#endif
+    }
+    fd_ = fd;
+  }
+
+ private:
+  int fd_ = -1;
+};
+
+// SkipTempFileTests returns true and prints a warning if tests involving
+// temporary files should be skipped because of platform issues.
+bool SkipTempFileTests();
+
+// TemporaryFile manages a temporary file for testing.
+class TemporaryFile {
+ public:
+  TemporaryFile() = default;
+  ~TemporaryFile();
+
+  TemporaryFile(TemporaryFile &other) { *this = std::move(other); }
+  TemporaryFile& operator=(TemporaryFile&&other) {
+    // Ensure |path_| is empty so it doesn't try to delete the File.
+    path_ = std::exchange(other.path_, {});
+    return *this;
+  }
+
+  // Init initializes the temporary file with the specified content. It returns
+  // true on success and false on error. On error, callers should call
+  // |IgnoreTempFileErrors| to determine whether to ignore the error.
+  bool Init(bssl::Span<const uint8_t> content = {});
+  bool Init(const std::string &content) {
+    return Init(bssl::MakeConstSpan(
+        reinterpret_cast<const uint8_t *>(content.data()), content.size()));
+  }
+
+  // Open opens the file as a |FILE| with the specified mode.
+  ScopedFILE Open(const char *mode) const;
+
+  // Open opens the file as a file descriptor with the specified flags.
+  ScopedFD OpenFD(int flags) const;
+
+  // path returns the path to the temporary file.
+  const std::string &path() const { return path_; }
+
+ private:
+  std::string path_;
+};
+
+#endif  // OPENSSL_HEADER_CRYPTO_TEST_FILE_UTIL_H
diff --git a/sources.cmake b/sources.cmake
index d32f24b..262b3fd 100644
--- a/sources.cmake
+++ b/sources.cmake
@@ -433,6 +433,7 @@
 
   crypto/test/abi_test.cc
   crypto/test/file_test.cc
+  crypto/test/file_util.cc
   crypto/test/test_util.cc
   crypto/test/wycheproof_util.cc
 )
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 3595204..0b4ad3c 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -43,6 +43,7 @@
 
 #include "internal.h"
 #include "../crypto/internal.h"
+#include "../crypto/test/file_util.h"
 #include "../crypto/test/test_util.h"
 
 #if defined(OPENSSL_WINDOWS)
@@ -8728,14 +8729,17 @@
   }
 }
 
-#if defined(OPENSSL_LINUX) || defined(OPENSSL_APPLE)
 TEST(SSLTest, EmptyClientCAList) {
-  // Use /dev/null on POSIX systems as an empty file.
+  if (SkipTempFileTests()) {
+    GTEST_SKIP();
+  }
+
+  TemporaryFile empty;
+  ASSERT_TRUE(empty.Init());
   bssl::UniquePtr<STACK_OF(X509_NAME)> names(
-      SSL_load_client_CA_file("/dev/null"));
+      SSL_load_client_CA_file(empty.path().c_str()));
   EXPECT_FALSE(names);
 }
-#endif  // OPENSSL_LINUX || OPENSSL_APPLE
 
 TEST(SSLTest, EmptyWriteBlockedOnHandshakeData) {
   bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_method()));