From bf1ea4e7996357c55fea2bde18daf2a20abd1591 Mon Sep 17 00:00:00 2001
From: Josh Megnauth <jo.sh@tutanota.com>
Date: Sat, 2 Nov 2024 12:21:47 +0000
Subject: [PATCH] Set `h_errno` for gethostbyname/addr

---
 include/bits/netdb.h    |   9 +++
 src/header/netdb/mod.rs | 118 ++++++++++++++++++++++++++++++++++++----
 tests/netdb/netdb.c     |  29 ++++++++++
 3 files changed, 146 insertions(+), 10 deletions(-)

diff --git a/include/bits/netdb.h b/include/bits/netdb.h
index 698e0e216..811097a42 100644
--- a/include/bits/netdb.h
+++ b/include/bits/netdb.h
@@ -1,6 +1,15 @@
 #ifndef _BITS_NETDB_H
 #define _BITS_NETDB_H
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define h_errno (*__h_errno_location())
 # define        h_addr  h_addr_list[0] /* Address, for backward compatibility.*/
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif /* _BITS_NETDB_H */
diff --git a/src/header/netdb/mod.rs b/src/header/netdb/mod.rs
index d5ee94a52..c01579af9 100644
--- a/src/header/netdb/mod.rs
+++ b/src/header/netdb/mod.rs
@@ -3,6 +3,8 @@
 mod dns;
 
 use core::{
+    cell::Cell,
+    fmt::Write,
     mem, ptr, slice,
     str::{self, FromStr},
 };
@@ -132,14 +134,18 @@ pub static mut NET_ADDR: Option<u32> = None;
 static mut N_POS: usize = 0;
 static mut NET_STAYOPEN: c_int = 0;
 
-#[allow(non_upper_case_globals)]
-#[no_mangle]
-pub static mut h_errno: c_int = 0;
+#[thread_local]
+pub static H_ERRNO: Cell<c_int> = Cell::new(0);
+const H_UNSET: c_int = 0;
 pub const HOST_NOT_FOUND: c_int = 1;
 pub const NO_DATA: c_int = 2;
 pub const NO_RECOVERY: c_int = 3;
 pub const TRY_AGAIN: c_int = 4;
 
+// Expected length of addresses
+const SOCKLEN_AF_INET4: socklen_t = 4;
+const SOCKLEN_AF_INET6: socklen_t = 16;
+
 static mut PROTODB: c_int = 0;
 static mut PROTO_ENTRY: protoent = protoent {
     p_name: ptr::null_mut(),
@@ -188,12 +194,44 @@ pub unsafe extern "C" fn endservent() {
     SERVDB = 0;
 }
 
+/// Resolve a host name from a given network address.
+///
+/// # Arguments
+/// * `v` - Address to resolve as a non-null [`in_addr`]
+/// * `length` -
+/// * `format` - AF_INET or AF_INET6
+///
+/// # Safety
+/// * `v` must be a valid pointer.
+/// * `length` must correctly match the size of `v` as expected by `format` (usually 4 or 16).
+/// * This function is not reentrant and may modify static data.
+///
+/// # Panics
+/// Panics if `v` is a null pointer.
+///
+/// # Deprecation
+/// Deprecated as of POSIX.1-2001 and removed in POSIX.1-2008.
+/// New code should use [`getaddrinfo`] instead.
 #[no_mangle]
+#[deprecated]
 pub unsafe extern "C" fn gethostbyaddr(
     v: *const c_void,
     length: socklen_t,
     format: c_int,
 ) -> *mut hostent {
+    assert!(
+        !v.is_null(),
+        "`gethostbyaddr()` called with null `v` (in_addr)"
+    );
+    // Uncomment if optional IPv6 support is added
+    // if length != SOCKLEN_AF_INET4 || length != SOCKLEN_AF_INET6 {
+    //     H_ERRNO.set(NO_RECOVERY);
+    //     return ptr::null_mut();
+    // }
+    if length != SOCKLEN_AF_INET4 {
+        H_ERRNO.set(NO_RECOVERY);
+        return ptr::null_mut();
+    }
     let addr: in_addr = *(v as *mut in_addr);
 
     // check the hosts file first
@@ -246,24 +284,39 @@ pub unsafe extern "C" fn gethostbyaddr(
         // `glibc` sets errno if an address doesn't have a host name
         // `musl` uses the address as the host name in said case
         Ok(None) => {
-            // TODO: Set h_errno instead to match spec
-            platform::ERRNO.set(EIO);
+            H_ERRNO.set(HOST_NOT_FOUND);
             ptr::null_mut()
         }
         Err(e) => {
-            platform::ERRNO.set(e);
+            // TODO: Better error separation in lookup_addr
+            H_ERRNO.set(NO_RECOVERY);
             ptr::null_mut()
         }
     }
 }
 
+/// Resolve host information by name or IP address.
+///
+/// # Arguments
+/// * `name` - Host name or IP address.
+///
+/// # Safety
+/// `name` must be a valid string.
+/// This function is not reentrant and may modify static data.
+///
+/// # Panics
+/// Panics if `name` is a null pointer.
+///
+/// # Deprecation
+/// Deprecated as of POSIX.1-2001 and removed in POSIX.1-2008.
+/// New code should use [`getaddrinfo`] instead.
 #[no_mangle]
+#[deprecated]
 pub unsafe extern "C" fn gethostbyname(name: *const c_char) -> *mut hostent {
     let name_cstr =
         CStr::from_nullable_ptr(name).expect("gethostbyname() called with a NULL pointer");
     let Ok(name_str) = str::from_utf8(name_cstr.to_bytes()) else {
-        // TODO Set h_errno instead of errno (spec)
-        platform::ERRNO.set(EINVAL);
+        H_ERRNO.set(NO_RECOVERY);
         return ptr::null_mut();
     };
 
@@ -321,14 +374,14 @@ pub unsafe extern "C" fn gethostbyname(name: *const c_char) -> *mut hostent {
     let mut host = match lookup_host(name_str) {
         Ok(lookuphost) => lookuphost,
         Err(e) => {
-            platform::ERRNO.set(e);
+            H_ERRNO.set(NO_RECOVERY);
             return ptr::null_mut();
         }
     };
     let host_addr = match host.next() {
         Some(result) => result,
         None => {
-            platform::ERRNO.set(ENOENT);
+            H_ERRNO.set(HOST_NOT_FOUND);
             return ptr::null_mut();
         }
     };
@@ -902,9 +955,18 @@ pub extern "C" fn gai_strerror(errcode: c_int) -> *const c_char {
     .as_ptr()
 }
 
+/// Provide a pointer to relibc's internal [`H_ERRNO`].
+#[no_mangle]
+#[deprecated]
+pub extern "C" fn __h_errno_location() -> *mut c_int {
+    H_ERRNO.as_ptr()
+}
+
 #[no_mangle]
+#[deprecated]
 pub extern "C" fn hstrerror(errcode: c_int) -> *const c_char {
     match errcode {
+        H_UNSET => c_str!("Resolver error unset"),
         HOST_NOT_FOUND => c_str!("Unknown hostname"),
         NO_DATA => c_str!("No address for hostname"),
         NO_RECOVERY => c_str!("Unknown server error"),
@@ -913,3 +975,39 @@ pub extern "C" fn hstrerror(errcode: c_int) -> *const c_char {
     }
     .as_ptr()
 }
+
+/// Print error message associated with [`H_ERRNO`] to stderr.
+///
+/// # Arguments
+/// * `prefix` - An optional prefix to prepend to the error message. May be null or an empty
+/// (`""`) C string.
+///
+/// # Safety
+/// Like [`crate::header::stdio::perror`], `prefix` should be a valid, NUL terminated C string if
+/// used. The caller may safely call this function with a null pointer.
+///
+/// # Deprecation
+/// [`H_ERRNO`], [`hstrerror`], [`herror`], and other functions are deprecated as of
+/// POSIX.1-2001 and removed as of POSIX.1-2008. These functions are provided for backwards
+/// compatibility but should not be used by new code.
+#[no_mangle]
+#[deprecated]
+pub extern "C" fn herror(prefix: *const c_char) {
+    let code = H_ERRNO.get();
+    // Safety: `hstrerror` handles every error code case and always returns a valid C string
+    let error = unsafe {
+        let msg_cstr = CStr::from_ptr(hstrerror(code));
+        str::from_utf8_unchecked(msg_cstr.to_bytes())
+    };
+
+    let mut writer = platform::FileWriter(2);
+    // Prefix is optional
+    match unsafe { CStr::from_nullable_ptr(prefix) }
+        .and_then(|prefix| str::from_utf8(prefix.to_bytes()).ok())
+    {
+        Some(prefix) if !prefix.is_empty() => writer
+            .write_fmt(format_args!("{prefix}: {error}\n"))
+            .unwrap(),
+        _ => writer.write_fmt(format_args!("{error}\n")).unwrap(),
+    }
+}
diff --git a/tests/netdb/netdb.c b/tests/netdb/netdb.c
index d675fc010..2ccf70ec0 100644
--- a/tests/netdb/netdb.c
+++ b/tests/netdb/netdb.c
@@ -26,6 +26,7 @@
 
 #include <netdb.h>
 #include <stdio.h>
+#include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
 #include <arpa/inet.h>
@@ -37,6 +38,8 @@
 
 #include "test_helpers.h"
 
+#define ADDR_SIZE sizeof(uint8_t) * 4
+
 int error_count;
 static void
 output_servent (const char *call, struct servent *sptr)
@@ -229,12 +232,38 @@ test_network (void)
   while (nptr != NULL);
   setnetent (0);
 }
+static void
+test_h_errno (void) {
+  const uint8_t addr[] = {0, 0, 0, 0};
+  struct hostent *host = gethostbyaddr(addr, ADDR_SIZE, AF_INET);
+  if (host) {
+    ++error_count;
+  }
+
+  int err = h_errno;
+  herror("Prefix test");
+  if (err != HOST_NOT_FOUND) {
+    ++error_count;
+  }
+
+  host = gethostbyname("");
+  if (host) {
+    ++error_count;
+  }
+
+  err = h_errno;
+  herror("");
+  if (err != HOST_NOT_FOUND) {
+    ++error_count;
+  }
+}
 static int
 do_test (void)
 {
   /*
     setdb ("db");
   */
+  test_h_errno ();
   test_hosts ();
   test_network ();
   test_protocols ();
-- 
GitLab