<sys/stat.h>: add lchmod().

Bug: http://b/361334945
Change-Id: I6a76db7bd2f657ad9b058c2672bf7a4e687017f9
diff --git a/docs/status.md b/docs/status.md
index 174c005..d4bb5ca 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -57,6 +57,7 @@
 
 New libc functions in API level 36:
   * `qsort_r`, `sig2str`/`str2sig` (POSIX Issue 8 additions).
+  * GNU/BSD extension `lchmod`.
   * New system call wrapper: `mseal` (`<sys/mman.h>`).
 
 New libc functions in V (API level 35):
diff --git a/libc/Android.bp b/libc/Android.bp
index 107db88..a81999d 100644
--- a/libc/Android.bp
+++ b/libc/Android.bp
@@ -913,6 +913,7 @@
         "bionic/isatty.cpp",
         "bionic/killpg.cpp",
         "bionic/langinfo.cpp",
+        "bionic/lchmod.cpp",
         "bionic/lchown.cpp",
         "bionic/lfs64_support.cpp",
         "bionic/libc_init_common.cpp",
diff --git a/libc/bionic/lchmod.cpp b/libc/bionic/lchmod.cpp
new file mode 100644
index 0000000..928a724
--- /dev/null
+++ b/libc/bionic/lchmod.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+int lchmod(const char* path, mode_t mode) {
+  return fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW);
+}
diff --git a/libc/include/sys/stat.h b/libc/include/sys/stat.h
index 0ea596b..bb188fe 100644
--- a/libc/include/sys/stat.h
+++ b/libc/include/sys/stat.h
@@ -99,7 +99,13 @@
 
 #endif
 
+/** The file information returned by fstat()/fstatat()/lstat()/stat(). */
 struct stat { __STAT64_BODY };
+
+/**
+ * A synonym for `struct stat` on Android,
+ * provided for source compatibility with other systems.
+ */
 struct stat64 { __STAT64_BODY };
 
 #undef __STAT64_BODY
@@ -136,32 +142,145 @@
 #define S_TYPEISSHM(__sb) 0
 #define S_TYPEISTMO(__sb) 0
 
+/**
+ * [chmod(2)](https://man7.org/linux/man-pages/man2/chmod.2.html)
+ * changes the mode of a file given a path.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int chmod(const char* _Nonnull __path, mode_t __mode);
+
+/**
+ * [fchmod(2)](https://man7.org/linux/man-pages/man2/fchmod.2.html)
+ * changes the mode of a file given a file descriptor.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int fchmod(int __fd, mode_t __mode);
+
+/**
+ * [fchmodat(2)](https://man7.org/linux/man-pages/man2/fchmodat.2.html)
+ * changes the mode of a file.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
+int fchmodat(int __dir_fd, const char* _Nonnull __path, mode_t __mode, int __flags);
+
+/**
+ * [chmod(2)](https://man7.org/linux/man-pages/man2/chmod.2.html)
+ * changes the mode of a file given a path, without following symlinks.
+ *
+ * Equivalent to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`.
+ *
+ * Available since API 36.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
+int lchmod(const char* _Nonnull __path, mode_t __mode) __INTRODUCED_IN(36);
+
+/**
+ * [mkdir(2)](https://man7.org/linux/man-pages/man2/mkdir.2.html)
+ * creates a directory.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int mkdir(const char* _Nonnull __path, mode_t __mode);
 
+/**
+ * [mkdirat(2)](https://man7.org/linux/man-pages/man2/mkdirat.2.html)
+ * creates a directory.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
+int mkdirat(int __dir_fd, const char* _Nonnull __path, mode_t __mode);
+
+/**
+ * [fstat(2)](https://man7.org/linux/man-pages/man2/fstat.2.html)
+ * gets file status given a file descriptor.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int fstat(int __fd, struct stat* _Nonnull __buf);
+
+/** An alias for fstat(). */
 int fstat64(int __fd, struct stat64* _Nonnull __buf);
+
+/**
+ * [fstatat(2)](https://man7.org/linux/man-pages/man2/fstatat.2.html)
+ * gets file status.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int fstatat(int __dir_fd, const char* _Nonnull __path, struct stat* _Nonnull __buf, int __flags);
+
+/** An alias for fstatat(). */
 int fstatat64(int __dir_fd, const char* _Nonnull __path, struct stat64* _Nonnull __buf, int __flags);
+
+/**
+ * [lstat(2)](https://man7.org/linux/man-pages/man2/lstat.2.html)
+ * gets file status given a path, without following symlinks.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int lstat(const char* _Nonnull __path, struct stat* _Nonnull __buf);
+
+/** An alias for lstat(). */
 int lstat64(const char* _Nonnull __path, struct stat64* _Nonnull __buf);
+
+/**
+ * [stat(2)](https://man7.org/linux/man-pages/man2/stat.2.html)
+ * gets file status given a path.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int stat(const char* _Nonnull __path, struct stat* _Nonnull __buf);
+
+/** An alias for stat(). */
 int stat64(const char* _Nonnull __path, struct stat64* _Nonnull __buf);
 
+/**
+ * [mknod(2)](https://man7.org/linux/man-pages/man2/mknod.2.html)
+ * creates a directory, special, or regular file.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int mknod(const char* _Nonnull __path, mode_t __mode, dev_t __dev);
+
+/**
+ * [mknodat(2)](https://man7.org/linux/man-pages/man2/mknodat.2.html)
+ * creates a directory, special, or regular file.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
+int mknodat(int __dir_fd, const char* _Nonnull __path, mode_t __mode, dev_t __dev);
+
+/**
+ * [umask(2)](https://man7.org/linux/man-pages/man2/umask.2.html)
+ * gets and sets the process-wide file mode creation mask.
+ *
+ * Returns the previous file mode creation mask.
+ */
 mode_t umask(mode_t __mask);
 
 #if defined(__BIONIC_INCLUDE_FORTIFY_HEADERS)
 #include <bits/fortify/stat.h>
 #endif
 
+/**
+ * [mkfifo(2)](https://man7.org/linux/man-pages/man2/mkfifo.2.html)
+ * creates a FIFO.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
 int mkfifo(const char* _Nonnull __path, mode_t __mode);
-int mkfifoat(int __dir_fd, const char* _Nonnull __path, mode_t __mode) __INTRODUCED_IN(23);
 
-int fchmodat(int __dir_fd, const char* _Nonnull __path, mode_t __mode, int __flags);
-int mkdirat(int __dir_fd, const char* _Nonnull __path, mode_t __mode);
-int mknodat(int __dir_fd, const char* _Nonnull __path, mode_t __mode, dev_t __dev);
+/**
+ * [mkfifoat(2)](https://man7.org/linux/man-pages/man2/mkfifoat.2.html)
+ * creates a FIFO.
+ *
+ * Returns 0 on success and returns -1 and sets `errno` on failure.
+ */
+int mkfifoat(int __dir_fd, const char* _Nonnull __path, mode_t __mode) __INTRODUCED_IN(23);
 
 /**
  * Used in the tv_nsec field of an argument to utimensat()/futimens()
diff --git a/libc/libc.map.txt b/libc/libc.map.txt
index 8bb84eb..0d2e42f 100644
--- a/libc/libc.map.txt
+++ b/libc/libc.map.txt
@@ -591,6 +591,7 @@
     killpg;
     klogctl;
     labs;
+    lchmod; # introduced=36
     lchown;
     lcong48; # introduced=23
     ldexp;
diff --git a/tests/sys_stat_test.cpp b/tests/sys_stat_test.cpp
index 126f469..50c50df 100644
--- a/tests/sys_stat_test.cpp
+++ b/tests/sys_stat_test.cpp
@@ -151,12 +151,12 @@
   ASSERT_ERRNO(EINVAL);
 }
 
-TEST(sys_stat, fchmodat_nonexistant_file) {
+TEST(sys_stat, fchmodat_nonexistent_file) {
   ASSERT_EQ(-1, fchmodat(AT_FDCWD, "/blah", 0751, 0));
   ASSERT_ERRNO(ENOENT);
 }
 
-TEST(sys_stat, fchmodat_AT_SYMLINK_NOFOLLOW_nonexistant_file) {
+TEST(sys_stat, fchmodat_AT_SYMLINK_NOFOLLOW_nonexistent_file) {
   ASSERT_EQ(-1, fchmodat(AT_FDCWD, "/blah", 0751, AT_SYMLINK_NOFOLLOW));
 #if defined(__BIONIC__)
   ASSERT_ERRNO(ENOENT);
@@ -305,7 +305,7 @@
   ASSERT_EQ(0, faccessat(AT_FDCWD, "/dev/null", R_OK|W_OK, 0));
 }
 
-TEST(sys_stat, faccessat_nonexistant) {
+TEST(sys_stat, faccessat_nonexistent) {
   ASSERT_EQ(-1, faccessat(AT_FDCWD, "/blah", F_OK, AT_SYMLINK_NOFOLLOW));
 #if defined(__BIONIC__)
   // Android doesn't support AT_SYMLINK_NOFOLLOW
@@ -314,3 +314,26 @@
   ASSERT_ERRNO(ENOENT);
 #endif
 }
+
+TEST(sys_stat, lchmod) {
+  TemporaryFile tf;
+  struct stat tf_sb;
+  ASSERT_EQ(0, stat(tf.path, &tf_sb));
+
+  char linkname[255];
+  snprintf(linkname, sizeof(linkname), "%s.link", tf.path);
+
+  ASSERT_EQ(0, symlink(tf.path, linkname));
+  int result = lchmod(linkname, 0751);
+  // Whether or not chmod is allowed on a symlink depends on the kernel.
+  if (result == 0) {
+    AssertSymlinkModeEquals(0751, linkname);
+  } else {
+    ASSERT_EQ(-1, result);
+    ASSERT_ERRNO(ENOTSUP);
+  }
+
+  // The target file mode shouldn't be modified.
+  AssertFileModeEquals(tf_sb.st_mode, tf.path);
+  unlink(linkname);
+}