diff --git a/System/Posix/Env/Internal.hsc b/System/Posix/Env/Internal.hsc
--- a/System/Posix/Env/Internal.hsc
+++ b/System/Posix/Env/Internal.hsc
@@ -22,8 +22,11 @@
 foreign import ccall unsafe "_NSGetEnviron"
    nsGetEnviron :: IO (Ptr (Ptr CString))
 #else
-getCEnviron = peek c_environ_p
+getCEnviron = _getCEnviron
 
-foreign import ccall unsafe "&environ"
-   c_environ_p :: Ptr (Ptr CString)
+-- N.B. we cannot import `environ` directly in Haskell as it may be a weak symbol
+-- which requires special treatment by the compiler, which GHC is not equipped to
+-- provide. See GHC #24011.
+foreign import ccall unsafe "__hsunix_get_environ"
+   _getCEnviron :: IO (Ptr CString)
 #endif
diff --git a/System/Posix/Files.hsc b/System/Posix/Files.hsc
--- a/System/Posix/Files.hsc
+++ b/System/Posix/Files.hsc
@@ -1,5 +1,6 @@
 {-# LANGUAGE Safe #-}
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE PatternSynonyms #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -62,6 +63,69 @@
     fileBlockSize,
     fileBlocks,
 
+    -- * Extended file status
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    -- ** Obtaining extended file status
+    getExtendedFileStatus,
+    -- ** Flags
+    StatxFlags(..),
+    defaultStatxFlags,
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    -- ** Mask
+    StatxMask(..),
+    defaultStatxMask,
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxBasicStats,
+    pattern StatxBtime,
+    pattern StatxMntId,
+    pattern StatxAll,
+    -- ** Querying extended file status
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Creation
     createNamedPipe,
     createDevice,
@@ -188,6 +252,25 @@
     withFilePath path $ \s ->
       throwErrnoPathIfMinus1Retry_ "getFileStatus" path (c_stat s p)
   return (FileStatus fp)
+
+-- | Gets extended file status information.
+--
+-- The target file to open is identified in one of the following ways:
+--
+-- - If @pathname@ begins with a slash, then it is an absolute pathname that identifies the target file. In this case, @dirfd@ is ignored
+-- - If @pathname@ is a string that begins with a character other than a slash and @dirfd@ is a file descriptor that refers to a
+--   directory, then pathname is a relative pathname that is interpreted relative to the directory referred to by dirfd.
+--   (See @openat(2)@ for an explanation of why this is useful.)
+-- - If @pathname@ is an empty string and the 'EmptyPath' flag is specified in flags (see below), then the target file is
+--   the one referred to by the file descriptor @dirfd@.
+--
+-- Note: calls @statx@.
+getExtendedFileStatus :: Maybe Fd    -- ^ Optional directory file descriptor (@dirfd@)
+                      -> FilePath    -- ^ @pathname@ to open
+                      -> StatxFlags  -- ^ flags
+                      -> StatxMask   -- ^ mask
+                      -> IO ExtendedFileStatus
+getExtendedFileStatus mfd path flags masks = withFilePath path $ \s -> getExtendedFileStatus_ mfd s flags masks
 
 -- | Acts as 'getFileStatus' except when the 'FilePath' refers to a symbolic
 -- link. In that case the @FileStatus@ information of the symbolic link itself
diff --git a/System/Posix/Files/ByteString.hsc b/System/Posix/Files/ByteString.hsc
--- a/System/Posix/Files/ByteString.hsc
+++ b/System/Posix/Files/ByteString.hsc
@@ -1,5 +1,6 @@
 {-# LANGUAGE Safe #-}
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE PatternSynonyms #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -62,6 +63,69 @@
     fileBlockSize,
     fileBlocks,
 
+    -- * Extended file status
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    -- ** Obtaining extended file status
+    getExtendedFileStatus,
+    -- ** Flags
+    StatxFlags(..),
+    defaultStatxFlags,
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    -- ** Mask
+    StatxMask(..),
+    defaultStatxMask,
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxBasicStats,
+    pattern StatxBtime,
+    pattern StatxMntId,
+    pattern StatxAll,
+    -- ** Querying extended file status
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Creation
     createNamedPipe,
     createDevice,
@@ -183,6 +247,25 @@
     withFilePath path $ \s ->
       throwErrnoPathIfMinus1Retry_ "getFileStatus" path (c_stat s p)
   return (FileStatus fp)
+
+-- | Gets extended file status information.
+--
+-- The target file to open is identified in one of the following ways:
+--
+-- - If @pathname@ begins with a slash, then it is an absolute pathname that identifies the target file. In this case, @dirfd@ is ignored
+-- - If @pathname@ is a string that begins with a character other than a slash and @dirfd@ is a file descriptor that refers to a
+--   directory, then pathname is a relative pathname that is interpreted relative to the directory referred to by dirfd.
+--   (See @openat(2)@ for an explanation of why this is useful.)
+-- - If @pathname@ is an empty string and the 'EmptyPath' flag is specified in flags (see below), then the target file is
+--   the one referred to by the file descriptor @dirfd@.
+--
+-- Note: calls @statx@.
+getExtendedFileStatus :: Maybe Fd     -- ^ Optional directory file descriptor (@dirfd@)
+                      -> RawFilePath  -- ^ @pathname@ to open
+                      -> StatxFlags   -- ^ flags
+                      -> StatxMask    -- ^ mask
+                      -> IO ExtendedFileStatus
+getExtendedFileStatus mfd path flags masks = withFilePath path $ \s -> getExtendedFileStatus_ mfd s flags masks
 
 -- | Acts as 'getFileStatus' except when the 'RawFilePath' refers to a symbolic
 -- link. In that case the @FileStatus@ information of the symbolic link itself
diff --git a/System/Posix/Files/Common.hsc b/System/Posix/Files/Common.hsc
--- a/System/Posix/Files/Common.hsc
+++ b/System/Posix/Files/Common.hsc
@@ -1,5 +1,9 @@
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE Trustworthy #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE NumDecimals #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -60,6 +64,65 @@
     fileBlockSize,
     fileBlocks,
 
+    -- * Extended file status
+    StatxFlags(..),
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    defaultStatxFlags,
+    StatxMask(..),
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxBtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxMntId,
+    pattern StatxBasicStats,
+    pattern StatxAll,
+    defaultStatxMask,
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    getExtendedFileStatus_,
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Setting file sizes
     setFdSize,
 
@@ -93,6 +156,11 @@
 import Data.Bits
 import Data.Int
 import Data.Ratio
+import Data.Word
+#ifdef HAVE_STATX
+import Data.Time.Clock (secondsToNominalDiffTime)
+import Data.Fixed (Fixed(..))
+#endif
 import Data.Time.Clock.POSIX (POSIXTime)
 import System.Posix.Internals
 import Foreign.C
@@ -103,7 +171,7 @@
 import Foreign.Ptr
 import Foreign.Storable
 
-#if !defined(HAVE_FCHMOD) || !defined(HAVE_CHOWN)
+#if !defined(HAVE_FCHMOD) || !defined(HAVE_CHOWN) || !defined(HAVE_STATX)
 import System.IO.Error ( ioeSetLocation )
 import GHC.IO.Exception ( unsupportedOperation )
 #endif
@@ -688,3 +756,593 @@
 
 foreign import ccall unsafe "fpathconf"
   c_fpathconf :: CInt -> CInt -> IO CLong
+
+
+-- -----------------------------------------------------------------------------
+-- statx
+--
+
+newtype {-# CTYPE "__u64" #-} CAttributes = CAttributes Word64
+ deriving (Read, Show, Eq, Ord, Storable, Num, Bits)
+
+-- | Statx flags.
+--
+-- See the pattern synonyms for possible flags. These are combined via `(<>)`.
+-- Flags can be tested via `(.&.)`.
+--
+-- The following flags influence pathname-based lookup:
+--
+-- - 'EmptyPath'
+-- - 'NoAutoMount'
+-- - 'SymlinkNoFollow'
+--
+-- The following flags can be used to control what sort of synchronization the kernel will do when querying a file on a remote filesystem:
+--
+-- - 'SyncAsStat'
+-- - 'ForceSync'
+-- - 'DontSync'
+newtype StatxFlags = StatxFlags CInt deriving (Read, Show, Eq, Ord, Integral, Num, Enum, Bits, Real)
+
+-- | ORs the flags.
+instance Semigroup StatxFlags where
+   a <> b = a .|. b
+
+instance Monoid StatxFlags where
+  mappend = (<>)
+  mempty = 0
+
+-- | If pathname to 'getExtendedFileStatus' is an empty string, operate on the file referred to by
+-- the 'Maybe Fd' argument.
+--
+-- In this case, it can refer to any type of file, not just a directory.
+pattern EmptyPath :: StatxFlags
+#ifdef AT_EMPTY_PATH
+pattern EmptyPath = StatxFlags (#const AT_EMPTY_PATH)
+#else
+pattern EmptyPath = StatxFlags 0
+#endif
+
+-- | Don't automount the terminal ("basename") component of pathname if it is a directory that is an automount point.
+-- This allows the caller to gather attributes of an automount point (rather than the location it would mount).
+-- This flag can be used in tools that scan directories to prevent mass-automounting of a directory of automount points.
+-- This flag has no effect if the mount point has already been mounted over.
+pattern NoAutoMount :: StatxFlags
+#ifdef AT_NO_AUTOMOUNT
+pattern NoAutoMount = StatxFlags (#const AT_NO_AUTOMOUNT)
+#else
+pattern NoAutoMount = StatxFlags 0
+#endif
+
+-- | If pathname is a symbolic link, do not dereference it: instead return information about the link itself, like @lstat(2)@.
+pattern SymlinkNoFollow :: StatxFlags
+#ifdef AT_SYMLINK_NOFOLLOW
+pattern SymlinkNoFollow = StatxFlags (#const AT_SYMLINK_NOFOLLOW)
+#else
+pattern SymlinkNoFollow = StatxFlags 0
+#endif
+
+-- | Do whatever @stat(2)@ does. This is the default and is very much filesystem-specific.
+pattern SyncAsStat :: StatxFlags
+#ifdef AT_STATX_SYNC_AS_STAT
+pattern SyncAsStat = StatxFlags (#const AT_STATX_SYNC_AS_STAT)
+#else
+pattern SyncAsStat = StatxFlags 0
+#endif
+
+-- | Force the attributes to be synchronized with the server.
+-- This may require that a network filesystem perform a data writeback to get the timestamps correct.
+pattern ForceSync :: StatxFlags
+#ifdef AT_STATX_FORCE_SYNC
+pattern ForceSync = StatxFlags (#const AT_STATX_FORCE_SYNC)
+#else
+pattern ForceSync = StatxFlags 0
+#endif
+
+-- | Don't synchronize anything, but rather just take whatever the system has cached if possible.
+-- This may mean that the information returned is approximate, but, on a network filesystem,
+-- it may not involve a round trip to the server - even if no lease is held.
+pattern DontSync :: StatxFlags
+#ifdef AT_STATX_DONT_SYNC
+pattern DontSync = StatxFlags (#const AT_STATX_DONT_SYNC)
+#else
+pattern DontSync = StatxFlags 0
+#endif
+
+defaultStatxFlags :: StatxFlags
+defaultStatxFlags = mempty
+
+-- | Mask argument to 'statx'. It's used to tell the kernel which fields the caller is interested in.
+--
+-- See the pattern synonyms for possible masks. These are combined via @(<>)@.
+-- Masks can be tested via `(.&.)`.
+newtype StatxMask = StatxMask CInt deriving (Read, Show, Eq, Ord, Integral, Num, Enum, Bits, Real)
+
+-- | ORs the masks.
+instance Semigroup StatxMask where
+   a <> b = a .|. b
+
+instance Monoid StatxMask where
+  mappend = (<>)
+  mempty = 0
+
+-- | Want @stx_mode & S_IFMT@.
+pattern StatxType :: StatxMask
+#ifdef STATX_TYPE
+pattern StatxType = StatxMask (#const STATX_TYPE)
+#else
+pattern StatxType = StatxMask 0
+#endif
+
+-- | Want @stx_mode & ~S_IFMT@.
+pattern StatxMode :: StatxMask
+#ifdef STATX_MODE
+pattern StatxMode = StatxMask (#const STATX_MODE)
+#else
+pattern StatxMode = StatxMask 0
+#endif
+
+-- | Want @stx_nlink@.
+pattern StatxNlink :: StatxMask
+#ifdef STATX_NLINK
+pattern StatxNlink = StatxMask (#const STATX_NLINK)
+#else
+pattern StatxNlink = StatxMask 0
+#endif
+
+-- | Want @stx_uid@.
+pattern StatxUid :: StatxMask
+#ifdef STATX_UID
+pattern StatxUid = StatxMask (#const STATX_UID)
+#else
+pattern StatxUid = StatxMask 0
+#endif
+
+-- | Want @stx_gid@.
+pattern StatxGid :: StatxMask
+#ifdef STATX_GID
+pattern StatxGid = StatxMask (#const STATX_GID)
+#else
+pattern StatxGid = StatxMask 0
+#endif
+
+-- | Want @stx_atime@.
+pattern StatxAtime :: StatxMask
+#ifdef STATX_ATIME
+pattern StatxAtime = StatxMask (#const STATX_ATIME)
+#else
+pattern StatxAtime = StatxMask 0
+#endif
+
+-- | Want @stx_mtime@.
+pattern StatxMtime :: StatxMask
+#ifdef STATX_MTIME
+pattern StatxMtime = StatxMask (#const STATX_MTIME)
+#else
+pattern StatxMtime = StatxMask 0
+#endif
+
+-- | Want @stx_ctime@.
+pattern StatxCtime :: StatxMask
+#ifdef STATX_CTIME
+pattern StatxCtime = StatxMask (#const STATX_CTIME)
+#else
+pattern StatxCtime = StatxMask 0
+#endif
+
+-- | Want @stx_btime@.
+pattern StatxBtime :: StatxMask
+#ifdef STATX_BTIME
+pattern StatxBtime = StatxMask (#const STATX_BTIME)
+#else
+pattern StatxBtime = StatxMask 0
+#endif
+
+-- | Want @stx_mnt_id@.
+pattern StatxMntId :: StatxMask
+#ifdef STATX_MNT_ID
+pattern StatxMntId = StatxMask (#const STATX_MNT_ID)
+#else
+pattern StatxMntId = StatxMask 0
+#endif
+
+-- | Want @stx_ino@.
+pattern StatxIno :: StatxMask
+#ifdef STATX_INO
+pattern StatxIno = StatxMask (#const STATX_INO)
+#else
+pattern StatxIno = StatxMask 0
+#endif
+
+-- | Want @stx_size@.
+pattern StatxSize :: StatxMask
+#ifdef STATX_SIZE
+pattern StatxSize = StatxMask (#const STATX_SIZE)
+#else
+pattern StatxSize = StatxMask 0
+#endif
+
+-- | Want @stx_blocks@.
+pattern StatxBlocks :: StatxMask
+#ifdef STATX_BLOCKS
+pattern StatxBlocks = StatxMask (#const STATX_BLOCKS)
+#else
+pattern StatxBlocks = StatxMask 0
+#endif
+
+-- | Want all of the above.
+pattern StatxBasicStats :: StatxMask
+#ifdef STATX_BASIC_STATS
+pattern StatxBasicStats = StatxMask (#const STATX_BASIC_STATS)
+#else
+pattern StatxBasicStats = StatxMask 0
+#endif
+
+-- | Want all currently available fields.
+pattern StatxAll :: StatxMask
+#ifdef STATX_ALL
+pattern StatxAll = StatxMask (#const STATX_ALL)
+#else
+pattern StatxAll = StatxMask 0
+#endif
+
+
+defaultStatxMask :: StatxMask
+defaultStatxMask = mempty
+
+newtype ExtendedFileStatus = ExtendedFileStatus (ForeignPtr CStatx) -- ^ The constructor is considered internal and may change.
+
+-- | The "preferred" block size for efficient filesystem I/O.
+-- (Writing to a file in smaller chunks may cause an inefficient read-mod‐ify-rewrite.)
+fileBlockSizeX             :: ExtendedFileStatus -> CBlkSize
+#if HAVE_STATX
+-- | Further status information about the file.
+fileAttributesX            :: ExtendedFileStatus -> CAttributes
+#endif
+-- | The number of hard links on a file.
+linkCountX                 :: ExtendedFileStatus -> CNlink
+-- | Te user ID of the owner of the file.
+fileOwnerX                 :: ExtendedFileStatus -> UserID
+-- | The ID of the group owner of the file.
+fileGroupX                 :: ExtendedFileStatus -> GroupID
+-- | The file type and mode.  See @inode(7)@ for details.
+fileModeX                  :: ExtendedFileStatus -> FileMode
+-- | The inode number of the file.
+fileIDX                    :: ExtendedFileStatus -> FileID
+-- | The size of the file (if it is a regular file or a symbolic link) in bytes.
+-- The size of a symbolic link is the length of the pathname it contains,
+-- without a terminating null byte.
+fileSizeX                  :: ExtendedFileStatus -> Word64
+-- | The  number of blocks allocated to the file on the medium, in 512-byte units.
+-- (This may be smaller than stx_size/512 when the file has holes.)
+fileBlocksX                :: ExtendedFileStatus -> Word64
+#if HAVE_STATX
+-- | A mask indicating which bits in 'fileAttributesX' are supported by the VFS and the filesystem.
+fileAttributesMaskX        :: ExtendedFileStatus -> CAttributes
+#endif
+-- | The file's last access timestamp.
+accessTimeHiResX           :: ExtendedFileStatus -> POSIXTime
+-- | The file's creation timestamp.
+creationTimeHiResX         :: ExtendedFileStatus -> POSIXTime
+-- | The file's last status change timestamp.
+statusChangeTimeHiResX     :: ExtendedFileStatus -> POSIXTime
+-- | The file's last modification timestamp.
+modificationTimeHiResX     :: ExtendedFileStatus -> POSIXTime
+-- | ID of the device on which this file resides.
+deviceIDX                  :: ExtendedFileStatus -> DeviceID
+-- | Describes the device that this file represents.
+specialDeviceIDX       :: ExtendedFileStatus -> DeviceID
+-- | The mount ID of the mount containing the file. This is the same number
+-- reported by name_to_handle_at(2) and corresponds to the number in the
+-- first field in one of the records in /proc/self/mountinfo.
+mountIDX               :: ExtendedFileStatus -> Word64
+-- | The file is compressed by the filesystem and may take extra resources to access.
+-- This is an extended attribute.
+fileCompressedX            :: ExtendedFileStatus -> Bool
+-- | The file cannot be modified: it cannot be deleted or renamed, no hard links can
+-- be created to this file and no data can be written to it. See @chattr(1)@.
+-- This is an extended attribute.
+fileImmutableX             :: ExtendedFileStatus -> Bool
+-- | The file can only be opened in append mode for writing. Random access writing is not permitted. See @chattr(1)@.
+-- This is an extended attribute.
+fileAppendX                :: ExtendedFileStatus -> Bool
+-- | File is not a candidate for backup when a backup program such as @dump(8)@ is run. See @chattr(1)@.
+-- This is an extended attribute.
+fileNoDumpX                :: ExtendedFileStatus -> Bool
+-- | A key is required for the file to be encrypted by the filesystem.
+-- This is an extended attribute.
+fileEncryptedX             :: ExtendedFileStatus -> Bool
+-- | The file has fs-verity enabled.  It cannot be written to, and all reads from it
+-- will be verified  against a cryptographic hash that covers the entire file (e.g., via a Merkle tree).
+-- This is an extended attribute.
+-- Since Linux 5.5.
+fileVerityX                :: ExtendedFileStatus -> Bool
+-- | The  file is in the DAX (cpu direct access) state.
+-- This is an extended attribute.
+-- Since Linux 5.8.
+fileDaxX                   :: ExtendedFileStatus -> Bool
+
+-- | Checks if this file is a block device.
+isBlockDeviceX     :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a character device.
+isCharacterDeviceX :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a named pipe device.
+isNamedPipeX       :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a regular file device.
+isRegularFileX     :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a directory device.
+isDirectoryX       :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a symbolic link device.
+isSymbolicLinkX    :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a socket device.
+isSocketX          :: ExtendedFileStatus -> Bool
+
+isBlockDeviceX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == blockSpecialMode
+isCharacterDeviceX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == characterSpecialMode
+isNamedPipeX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == namedPipeMode
+isRegularFileX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == regularFileMode
+isDirectoryX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == directoryMode
+isSymbolicLinkX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == symbolicLinkMode
+isSocketX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == socketMode
+
+#if HAVE_STATX
+testFlag :: ExtendedFileStatus -> CAttributes -> Bool
+testFlag ex flag =
+  let attributes = fileAttributesX ex
+      attributes_mask = fileAttributesMaskX ex
+  in (attributes .&. attributes_mask .&. flag) /= 0
+
+#ifdef STATX_ATTR_COMPRESSED
+fileCompressedX ex = testFlag ex (#const STATX_ATTR_COMPRESSED)
+#else
+{-# WARNING fileCompressedX "fileCompressedX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_COMPRESSED@)" #-}
+fileCompressedX _ = False
+#endif
+#ifdef STATX_ATTR_IMMUTABLE
+fileImmutableX ex = testFlag ex (#const STATX_ATTR_IMMUTABLE)
+#else
+{-# WARNING fileImmutableX "fileImmutableX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_IMMUTABLE@)" #-}
+fileImmutableX _ = False
+#endif
+#ifdef STATX_ATTR_APPEND
+fileAppendX ex = testFlag ex (#const STATX_ATTR_APPEND)
+#else
+{-# WARNING fileAppendX "fileAppendX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_APPEND@)" #-}
+fileAppendX _ = False
+#endif
+#ifdef STATX_ATTR_NODUMP
+fileNoDumpX ex = testFlag ex (#const STATX_ATTR_NODUMP)
+#else
+{-# WARNING fileNoDumpX "fileNoDumpX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_NODUMP@)" #-}
+fileNoDumpX _ = False
+#endif
+#ifdef STATX_ATTR_ENCRYPTED
+fileEncryptedX ex = testFlag ex (#const STATX_ATTR_ENCRYPTED)
+#else
+{-# WARNING fileEncryptedX "fileEncryptedX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_ENCRYPTED@)" #-}
+fileEncryptedX _ = False
+#endif
+#ifdef STATX_ATTR_VERITY
+fileVerityX ex = testFlag ex (#const STATX_ATTR_VERITY)
+#else
+{-# WARNING fileVerityX "fileVerityX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_VERITY@)" #-}
+fileVerityX _ = False
+#endif
+#ifdef STATX_ATTR_DAX
+fileDaxX ex = testFlag ex (#const STATX_ATTR_DAX)
+#else
+{-# WARNING fileDaxX "fileDaxX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_DAX@)" #-}
+fileDaxX _ = False
+#endif
+
+#ifdef HAVE_SYS_SYSMACROS_H
+deviceIDX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  major <- withForeignPtr statx $ (#peek struct statx, stx_dev_major) :: IO CUInt
+  minor <- withForeignPtr statx $ (#peek struct statx, stx_dev_minor) :: IO CUInt
+  c_makedev major minor
+#else
+{-# WARNING deviceIDX "deviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_SYS_SYSMACROS_H@)" #-}
+deviceIDX _ = error "deviceIDX not available on this platform"
+#endif
+#ifdef HAVE_SYS_SYSMACROS_H
+specialDeviceIDX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  major <- withForeignPtr statx $ (#peek struct statx, stx_rdev_major) :: IO CUInt
+  minor <- withForeignPtr statx $ (#peek struct statx, stx_rdev_minor) :: IO CUInt
+  c_makedev major minor
+#else
+{-# WARNING specialDeviceIDX "specialDeviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_SYS_SYSMACROS_H@)" #-}
+specialDeviceIDX _ = error "specialDeviceIDX not available on this platform"
+#endif
+#ifdef STATX_MNT_ID
+mountIDX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_mnt_id)
+#else
+{-# WARNING mountIDX "mountIDX: not available on this platform, will throw error (CPP guard: @#if STATX_MNT_ID@)" #-}
+mountIDX _ = error "mountIDX not available on this platform"
+#endif
+fileBlockSizeX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  r <- withForeignPtr statx $ (#peek struct statx, stx_blksize) :: IO Word32
+  return $ CBlkSize (fromIntegral r)
+fileAttributesX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_attributes)
+#ifdef STATX_NLINK
+linkCountX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  links <- withForeignPtr statx $ (#peek struct statx, stx_nlink) :: IO Word32
+  return $ CNlink (fromIntegral links)
+#else
+{-# WARNING linkCountX "linkCountX: not available on this platform, will throw error (CPP guard: @#if STATX_NLINK@)" #-}
+linkCountX _ = error "linkCountX not available on this platform"
+#endif
+#ifdef STATX_UID
+fileOwnerX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_uid)
+#else
+{-# WARNING fileOwnerX "fileOwnerX: not available on this platform, will throw error (CPP guard: @#if STATX_UID@)" #-}
+fileOwnerX _ = error "fileOwnerX not available on this platform"
+#endif
+#ifdef STATX_GID
+fileGroupX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_gid)
+#else
+{-# WARNING fileGroupX "fileGroupX: not available on this platform, will throw error (CPP guard: @#if STATX_GID@)" #-}
+fileGroupX _ = error "fileGroupX not available on this platform"
+#endif
+#ifdef STATX_MODE
+fileModeX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  r <- withForeignPtr statx $ (#peek struct statx, stx_mode) :: IO Word16
+  return $ CMode $ fromIntegral r
+#else
+{-# WARNING fileModeX "fileModeX: not available on this platform, will throw error (CPP guard: @#if STATX_MODE@)" #-}
+fileModeX _ = error "fileModeX not available on this platform"
+#endif
+#ifdef STATX_INO
+fileIDX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_ino)
+#else
+{-# WARNING fileIDX "fileIDX: not available on this platform, will throw error (CPP guard: @#if STATX_INO@)" #-}
+fileIDX _ = error "fileIDX not available on this platform"
+#endif
+#ifdef STATX_SIZE
+fileSizeX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_size) :: Word64
+#else
+{-# WARNING fileSizeX "fileSizeX: not available on this platform, will throw error (CPP guard: @#if STATX_SIZE@)" #-}
+fileSizeX _ = error "fileSizeX not available on this platform"
+#endif
+#ifdef STATX_BLOCKS
+fileBlocksX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_blocks) :: Word64
+#else
+{-# WARNING fileBlocksX "fileBlocksX: not available on this platform, will throw error (CPP guard: @#if STATX_BLOCKS@)" #-}
+fileBlocksX _ = error "fileBlocksX not available on this platform"
+#endif
+fileAttributesMaskX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_attributes_mask)
+#ifdef STATX_ATIME
+accessTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_atime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_atime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING accessTimeHiResX "accessTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_ATIME@)" #-}
+accessTimeHiResX _ = error "accessTimeHiResX not available on this platform"
+#endif
+#ifdef STATX_BTIME
+creationTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_btime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_btime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING creationTimeHiResX "creationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_BTIME@)" #-}
+creationTimeHiResX _ = error "creationTimeHiResX not available on this platform"
+#endif
+#ifdef STATX_CTIME
+statusChangeTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_ctime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_ctime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING statusChangeTimeHiResX "statusChangeTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_CTIME@)" #-}
+statusChangeTimeHiResX _ = error "statusChangeTimeHiResX not available on this platform"
+#endif
+#ifdef STATX_MTIME
+modificationTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_mtime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_mtime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING modificationTimeHiResX "modificationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_MTIME@)" #-}
+modificationTimeHiResX _ = error "modificationTimeHiResX not available on this platform"
+#endif
+
+timeHiResToNominalDiffTime :: EpochTime -> Int32 -> POSIXTime
+timeHiResToNominalDiffTime (CTime sec) nsec = secondsToNominalDiffTime $ MkFixed $ toInteger sec * 1e12 + toInteger nsec * 1e3
+
+#else
+{-# WARNING linkCountX "linkCountX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+linkCountX _ = error "linkCountX not available on this platform"
+{-# WARNING fileBlockSizeX "fileBlockSizeX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileBlockSizeX _ = error "fileBlockSizeX not available on this platform"
+{-# WARNING deviceIDX "deviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+deviceIDX _ = error "deviceIDX not available on this platform"
+{-# WARNING specialDeviceIDX "specialDeviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+specialDeviceIDX _ = error "specialDeviceIDX not available on this platform"
+{-# WARNING mountIDX "mountIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+mountIDX _ = error "mountIDX not available on this platform"
+{-# WARNING fileOwnerX "fileOwnerX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileOwnerX _ = error "fileOwnerX not available on this platform"
+{-# WARNING fileGroupX "fileGroupX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileGroupX _ = error "fileGroupX not available on this platform"
+{-# WARNING fileModeX "fileModeX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileModeX _ = error "fileModeX not available on this platform"
+{-# WARNING fileIDX "fileIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileIDX _ = error "fileIDX not available on this platform"
+{-# WARNING fileSizeX "fileSizeX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileSizeX _ = error "fileSizeX not available on this platform"
+{-# WARNING fileBlocksX "fileBlocksX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileBlocksX _ = error "fileBlocksX not available on this platform"
+{-# WARNING accessTimeHiResX "accessTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+accessTimeHiResX _ = error "accessTimeHiResX not available on this platform"
+{-# WARNING creationTimeHiResX "creationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+creationTimeHiResX _ = error "creationTimeHiResX not available on this platform"
+{-# WARNING statusChangeTimeHiResX "statusChangeTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+statusChangeTimeHiResX _ = error "statusChangeTimeHiResX not available on this platform"
+{-# WARNING modificationTimeHiResX "modificationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+modificationTimeHiResX _ = error "modificationTimeHiResX not available on this platform"
+{-# WARNING fileCompressedX "fileCompressedX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileCompressedX _ = False
+{-# WARNING fileImmutableX "fileImmutableX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileImmutableX _ = False
+{-# WARNING fileAppendX "fileAppendX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileAppendX _ = False
+{-# WARNING fileNoDumpX "fileNoDumpX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileNoDumpX _ = False
+{-# WARNING fileEncryptedX "fileEncryptedX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileEncryptedX _ = False
+{-# WARNING fileVerityX "fileVerityX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileVerityX _ = False
+{-# WARNING fileDaxX "fileDaxX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileDaxX _ = False
+#endif
+
+data {-# CTYPE "struct statx" #-} CStatx
+
+#ifdef HAVE_STATX
+foreign import capi unsafe "sys/stat.h statx"
+   c_statx :: CInt -> CFilePath -> CInt -> CInt -> Ptr CStatx -> IO CInt
+
+#ifdef HAVE_SYS_SYSMACROS_H
+foreign import capi unsafe "sys/sysmacros.h makedev"
+  c_makedev :: CUInt -> CUInt -> IO CDev
+#endif
+#endif
+
+
+getExtendedFileStatus_ :: Maybe Fd  -- ^ Optional directory file descriptor
+                       -> CString   -- ^ Pathname to open
+                       -> StatxFlags
+                       -> StatxMask
+                       -> IO ExtendedFileStatus
+#ifdef HAVE_STATX
+getExtendedFileStatus_ fdMay str (StatxFlags flags) (StatxMask masks) = do
+  fp <- mallocForeignPtrBytes (#const sizeof(struct statx))
+  withForeignPtr fp $ \p ->
+      throwErrnoIfMinus1_ "getExtendedFileStatus_" (c_statx c_fd str flags masks p)
+  return (ExtendedFileStatus fp)
+ where
+  c_fd = maybe (#const AT_FDCWD) (\ (Fd fd) -> fd) fdMay
+#else
+{-# WARNING getExtendedFileStatus_ "operation will throw 'IOError' \"unsupported operation\" (CPP guard: @#if HAVE_STATX@)" #-}
+getExtendedFileStatus_ _ _ _ _ = ioError (ioeSetLocation unsupportedOperation "getExtendedFileStatus")
+#endif
+
diff --git a/System/Posix/Files/PosixString.hsc b/System/Posix/Files/PosixString.hsc
--- a/System/Posix/Files/PosixString.hsc
+++ b/System/Posix/Files/PosixString.hsc
@@ -1,4 +1,5 @@
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE PatternSynonyms #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -58,6 +59,69 @@
     isBlockDevice, isCharacterDevice, isNamedPipe, isRegularFile,
     isDirectory, isSymbolicLink, isSocket,
 
+    -- * Extended file status
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    -- ** Obtaining extended file status
+    getExtendedFileStatus,
+    -- ** Flags
+    StatxFlags(..),
+    defaultStatxFlags,
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    -- ** Mask
+    StatxMask(..),
+    defaultStatxMask,
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxBasicStats,
+    pattern StatxBtime,
+    pattern StatxMntId,
+    pattern StatxAll,
+    -- ** Querying extended file status
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Creation
     createNamedPipe,
     createDevice,
@@ -102,7 +166,8 @@
      throwErrnoPathIfMinus1_ )
 
 import System.OsPath.Types
-import System.Posix.Files hiding (getFileStatus, getSymbolicLinkStatus, createNamedPipe, createDevice, createLink, removeLink, createSymbolicLink, readSymbolicLink, rename, setOwnerAndGroup, setSymbolicLinkOwnerAndGroup, setFileTimes, setSymbolicLinkTimesHiRes, touchFile, touchSymbolicLink, setFileSize, getPathVar, setFileMode, fileAccess, fileExist, setFdTimesHiRes, setFileTimesHiRes)
+import System.Posix.Files hiding (getFileStatus, getExtendedFileStatus, getSymbolicLinkStatus, createNamedPipe, createDevice, createLink, removeLink, createSymbolicLink, readSymbolicLink, rename, setOwnerAndGroup, setSymbolicLinkOwnerAndGroup, setFileTimes, setSymbolicLinkTimesHiRes, touchFile, touchSymbolicLink, setFileSize, getPathVar, setFileMode, fileAccess, fileExist, setFdTimesHiRes, setFileTimesHiRes)
+import System.Posix.Files.Common (getExtendedFileStatus_)
 import System.Posix.PosixPath.FilePath
 
 import Data.Time.Clock.POSIX (POSIXTime)
@@ -181,6 +246,25 @@
     withFilePath path $ \s ->
       throwErrnoPathIfMinus1Retry_ "getFileStatus" path (c_stat s p)
   return (Common.FileStatus fp)
+
+-- | Gets extended file status information.
+--
+-- The target file to open is identified in one of the following ways:
+--
+-- - If @pathname@ begins with a slash, then it is an absolute pathname that identifies the target file. In this case, @dirfd@ is ignored
+-- - If @pathname@ is a string that begins with a character other than a slash and @dirfd@ is a file descriptor that refers to a
+--   directory, then pathname is a relative pathname that is interpreted relative to the directory referred to by dirfd.
+--   (See @openat(2)@ for an explanation of why this is useful.)
+-- - If @pathname@ is an empty string and the 'EmptyPath' flag is specified in flags (see below), then the target file is
+--   the one referred to by the file descriptor @dirfd@.
+--
+-- Note: calls @statx@.
+getExtendedFileStatus :: Maybe Fd   -- ^ Optional directory file descriptor (@dirfd@)
+                      -> PosixPath  -- ^ @pathname@ to open
+                      -> StatxFlags -- ^ flags
+                      -> StatxMask  -- ^ mask
+                      -> IO ExtendedFileStatus
+getExtendedFileStatus mfd path flags masks = withFilePath path $ \s -> getExtendedFileStatus_ mfd s flags masks
 
 -- | Acts as 'getFileStatus' except when the 'PosixPath' refers to a symbolic
 -- link. In that case the @FileStatus@ information of the symbolic link itself
diff --git a/cbits/HsUnix.c b/cbits/HsUnix.c
--- a/cbits/HsUnix.c
+++ b/cbits/HsUnix.c
@@ -8,6 +8,8 @@
 
 #include "HsUnix.h"
 
+char **__hsunix_get_environ (void) {return environ;}
+
 #ifdef HAVE_RTLDNEXT
 void *__hsunix_rtldNext (void) {return RTLD_NEXT;}
 #endif
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
 # Changelog for [`unix` package](http://hackage.haskell.org/package/unix)
 
+## 2.8.3.0 *Oct 2023*
+
+  * add `getExtendedFileStatus` (based on `statx`) style functions
+  * drop support for GHC < 8.6
+  * Don't `foreign import` `environ`, see [GHC #24011](https://gitlab.haskell.org/ghc/ghc/-/issues/24011)
+
 ## 2.8.2.1 *Sep 2023*
 
   * Fix UB bug in `withFilePath` that causes it to error out (introduced in 2.8.2.0) wrt [#295](https://github.com/haskell/unix/issues/295)
diff --git a/configure b/configure
--- a/configure
+++ b/configure
@@ -4039,6 +4039,18 @@
 
 done
 
+for ac_header in sys/sysmacros.h
+do :
+  ac_fn_c_check_header_mongrel "$LINENO" "sys/sysmacros.h" "ac_cv_header_sys_sysmacros_h" "$ac_includes_default"
+if test "x$ac_cv_header_sys_sysmacros_h" = xyes; then :
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_SYS_SYSMACROS_H 1
+_ACEOF
+
+fi
+
+done
+
 for ac_header in bsd/libutil.h libutil.h pty.h utmp.h
 do :
   as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh`
@@ -4253,6 +4265,23 @@
 if test "x$ac_cv_type_struct_rlimit" = xyes; then :
 
 $as_echo "#define HAVE_STRUCT_RLIMIT 1" >>confdefs.h
+
+fi
+
+
+# check for statx
+ac_fn_c_check_func "$LINENO" "statx" "ac_cv_func_statx"
+if test "x$ac_cv_func_statx" = xyes; then :
+
+$as_echo "#define HAVE_STATX_FUN 1" >>confdefs.h
+
+fi
+
+ac_fn_c_check_type "$LINENO" "struct statx" "ac_cv_type_struct_statx" "#include <sys/stat.h>
+"
+if test "x$ac_cv_type_struct_statx" = xyes; then :
+
+$as_echo "#define HAVE_STRUCT_STATX 1" >>confdefs.h
 
 fi
 
diff --git a/configure.ac b/configure.ac
--- a/configure.ac
+++ b/configure.ac
@@ -23,6 +23,7 @@
 AC_CHECK_HEADERS([dirent.h fcntl.h grp.h limits.h pwd.h signal.h string.h])
 AC_CHECK_HEADERS([sys/resource.h sys/stat.h sys/times.h sys/time.h])
 AC_CHECK_HEADERS([sys/utsname.h sys/wait.h])
+AC_CHECK_HEADERS([sys/sysmacros.h])
 AC_CHECK_HEADERS([bsd/libutil.h libutil.h pty.h utmp.h])
 AC_CHECK_HEADERS([termios.h time.h unistd.h utime.h])
 
@@ -45,6 +46,10 @@
 AC_CHECK_FUNCS([chown fchdir fchmod])
 
 AC_CHECK_TYPE([struct rlimit],[AC_DEFINE([HAVE_STRUCT_RLIMIT],[1],[HAVE_STRUCT_RLIMIT])],[],[#include <sys/resource.h>])
+
+# check for statx
+AC_CHECK_FUNC([statx], [AC_DEFINE([HAVE_STATX_FUN],[1],[HAVE_STATX_FUN])],[],[#include <sys/stat.h>])
+AC_CHECK_TYPE([struct statx],[AC_DEFINE([HAVE_STRUCT_STATX],[1],[HAVE_STRUCT_STATX])],[],[#include <sys/stat.h>])
 
 AC_MSG_CHECKING(for F_GETLK from fcntl.h)
 AC_EGREP_CPP(yes,
diff --git a/include/HsUnix.h b/include/HsUnix.h
--- a/include/HsUnix.h
+++ b/include/HsUnix.h
@@ -90,6 +90,9 @@
 #ifdef HAVE_SIGNAL_H
 #include <signal.h>
 #endif
+#ifdef HAVE_SYS_SYSMACROS_H
+#include <sys/sysmacros.h>
+#endif
 
 /* defined in rts/posix/Signals.c */
 extern HsInt nocldstop;
@@ -121,6 +124,10 @@
 
 #ifdef HAVE_CLOCKS_PER_SEC
 clock_t __hsunix_clocks_per_second (void);
+#endif
+
+#if defined(HAVE_SYS_STAT_H) && defined(HAVE_STATX_FUN) && defined(HAVE_STRUCT_STATX)
+#define HAVE_STATX 1
 #endif
 
 #endif
diff --git a/include/HsUnixConfig.h.in b/include/HsUnixConfig.h.in
--- a/include/HsUnixConfig.h.in
+++ b/include/HsUnixConfig.h.in
@@ -306,6 +306,9 @@
 /* Define to 1 if you have the <signal.h> header file. */
 #undef HAVE_SIGNAL_H
 
+/* HAVE_STATX_FUN */
+#undef HAVE_STATX_FUN
+
 /* Define to 1 if you have the <stdint.h> header file. */
 #undef HAVE_STDINT_H
 
@@ -321,6 +324,9 @@
 /* HAVE_STRUCT_RLIMIT */
 #undef HAVE_STRUCT_RLIMIT
 
+/* HAVE_STRUCT_STATX */
+#undef HAVE_STRUCT_STATX
+
 /* Define to 1 if `st_atim' is a member of `struct stat'. */
 #undef HAVE_STRUCT_STAT_ST_ATIM
 
@@ -380,6 +386,9 @@
 
 /* Define to 1 if you have the <sys/stat.h> header file. */
 #undef HAVE_SYS_STAT_H
+
+/* Define to 1 if you have the <sys/sysmacros.h> header file. */
+#undef HAVE_SYS_SYSMACROS_H
 
 /* Define to 1 if you have the <sys/times.h> header file. */
 #undef HAVE_SYS_TIMES_H
diff --git a/tests/FileExtendedStatus.hs b/tests/FileExtendedStatus.hs
new file mode 100644
--- /dev/null
+++ b/tests/FileExtendedStatus.hs
@@ -0,0 +1,166 @@
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+
+module FileExtendedStatus (main) where
+
+import System.Posix.Files
+import System.Posix.Directory
+import System.Posix.IO
+import System.Posix.Types
+import Control.Exception as E
+import Control.Monad
+import Test.Tasty.HUnit
+
+main = do
+  cleanup
+  fs <- testRegular
+  ds <- testDir
+  testSymlink fs ds
+  testLink
+  cleanup
+
+regular       = "regular"
+dir           = "dir"
+slink_regular = "link-regular-symlink"
+hlink_regular = "link-regular-hardlink"
+link_dir      = "link-dir"
+
+testRegular = do
+  _ <- createFile regular ownerReadMode
+  (fs, _) <- getStatus regular
+  let expected = (False,False,False,True,False,False,False)
+      actual   = snd (statusExtendedElements fs)
+  when (actual /= expected) $
+    fail "unexpected file status bits for regular file"
+  return fs
+
+testDir = do
+  createDirectory dir ownerReadMode
+  (ds, _) <- getStatus dir
+  let expected = (False,False,False,False,True,False,False)
+      actual   = snd (statusExtendedElements ds)
+  when (actual /= expected) $
+    fail "unexpected file status bits for directory"
+  return ds
+
+testSymlink fs ds = do
+  createSymbolicLink regular slink_regular
+  createSymbolicLink dir     link_dir
+  (fs', ls)  <- getStatus slink_regular
+  (ds', lds) <- getStatus link_dir
+
+  let expected = (False,False,False,False,False,True,False)
+      actualF  = snd (statusExtendedElements ls)
+      actualD  = snd (statusExtendedElements lds)
+
+  when (actualF /= expected) $
+    fail "unexpected file status bits for symlink to regular file"
+
+  when (actualD /= expected) $
+    fail "unexpected file status bits for symlink to directory"
+
+  when (statusExtendedElements fs /= statusExtendedElements fs') $
+    fail "status for a file does not match when it's accessed via a symlink"
+
+  when (statusExtendedElements ds /= statusExtendedElements ds') $
+    fail "status for a directory does not match when it's accessed via a symlink"
+
+
+testLink = do
+  createLink regular hlink_regular
+  (fs, _)  <- getStatus regular -- we need to retrieve it again as creating the link causes it to change!
+  (fs', ls)  <- getStatus hlink_regular
+  snd (statusExtendedElements ls) @?= (
+                False, -- isBlockDevice
+                False, -- isCharacterDevice
+                False, -- isNamedPipe
+                True,  -- isRegularFile
+                False, -- isDirectory
+                False, -- isSymbolicLink
+                False) -- isSocket
+  linkCountX fs' @?= 2
+  statusExtendedElements fs @?= statusExtendedElements fs' -- status for a file should match when accessed via a link
+
+
+cleanup = do
+  ignoreIOExceptions $ removeDirectory dir
+  mapM_ (ignoreIOExceptions . removeLink)
+        [regular, hlink_regular, slink_regular, link_dir]
+
+ignoreIOExceptions io = io `E.catch`
+                        ((\_ -> return ()) :: IOException -> IO ())
+
+getStatus f = do
+  fs  <- getExtendedFileStatus Nothing f defaultStatxFlags defaultStatxMask
+  ls  <- getExtendedFileStatus Nothing f SymlinkNoFollow defaultStatxMask
+  fs' <- getFileStatus f
+
+  statusExtendedElementsMinimal fs @?= statusElementsMinimal fs'
+
+  return (fs, ls)
+
+-- Yay for 20-element tuples!
+statusExtendedElements fs = (,)
+  (fileBlockSizeX fs
+  ,linkCountX fs
+  ,fileOwnerX fs
+  ,fileGroupX fs
+  ,fileModeX fs
+  ,fileIDX fs
+  ,fileSizeX fs
+  ,accessTimeHiResX fs
+  ,creationTimeHiResX fs
+  ,statusChangeTimeHiResX fs
+  ,modificationTimeHiResX fs
+  )
+  (isBlockDeviceX fs
+  ,isCharacterDeviceX fs
+  ,isNamedPipeX fs
+  ,isRegularFileX fs
+  ,isDirectoryX fs
+  ,isSymbolicLinkX fs
+  ,isSocketX fs
+  )
+
+statusExtendedElementsMinimal fs = (,)
+  (fileModeX fs
+  ,deviceIDX fs
+  ,specialDeviceIDX fs
+  ,linkCountX fs
+  ,fileOwnerX fs
+  ,fileGroupX fs
+  ,COff (fromIntegral (fileSizeX fs))
+  ,Just $ CBlkCnt (fromIntegral (fileBlocksX fs))
+  ,accessTimeHiResX fs
+  ,statusChangeTimeHiResX fs
+  ,modificationTimeHiResX fs
+  )
+  (isBlockDeviceX fs
+  ,isCharacterDeviceX fs
+  ,isNamedPipeX fs
+  ,isRegularFileX fs
+  ,isDirectoryX fs
+  ,isSymbolicLinkX fs
+  ,isSocketX fs
+  )
+
+statusElementsMinimal fs = (,)
+  (fileMode fs
+  ,deviceID fs
+  ,specialDeviceID fs
+  ,linkCount fs
+  ,fileOwner fs
+  ,fileGroup fs
+  ,fileSize fs
+  ,fileBlocks fs
+  ,accessTimeHiRes fs
+  ,statusChangeTimeHiRes fs
+  ,modificationTimeHiRes fs
+  )
+  (isBlockDevice fs
+  ,isCharacterDevice fs
+  ,isNamedPipe fs
+  ,isRegularFile fs
+  ,isDirectory fs
+  ,isSymbolicLink fs
+  ,isSocket fs
+  )
diff --git a/tests/Test.hs b/tests/Test.hs
deleted file mode 100644
--- a/tests/Test.hs
+++ /dev/null
@@ -1,283 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE CPP #-}
-
-{-# OPTIONS_GHC -fno-warn-deprecations #-}
-{-# OPTIONS_GHC -fno-warn-unused-imports #-}
-
-module Main (main) where
-
-import Control.Applicative
-import Control.Concurrent
-import qualified Control.Exception as E
-import Control.Monad
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Short as Sh
-import Data.List (sort)
-import System.Exit
-import System.IO
-import System.OsString.Internal.Types
-import System.Posix
-import qualified System.Posix.Env.ByteString as ByteString
-import qualified System.Posix.ByteString.FilePath as BSFP
-import qualified System.Posix.PosixPath.FilePath as PPFP
-import Test.Tasty
-import Test.Tasty.HUnit
-import Test.Tasty.QuickCheck
-
-import qualified FileStatus
-import qualified FileStatusByteString
-import qualified Signals001
-
-main :: IO ()
-main = defaultMain $ testGroup "All"
-  [ testGroup "Common" -- common to JS and native platforms
-    [ fileExist01
-    ]
-#if !defined(javascript_HOST_ARCH)
-  , testGroup "Native"
-    [ executeFile001               -- JS: missing "pipe"
-    , fileStatus                   -- JS: missing "openat"
-    , fileStatusByteString         -- JS: missing "openat"
-    , getEnvironment01             -- JS: missing "environ"
-    , testSystemPosixEnvByteString -- JS: missing "environ"
-    , getGroupEntry                -- JS: missing "sysconf"
-    , getUserEntry                 -- JS: missing "sysconf"
-    , processGroup001              -- JS: missing "getpgrp"
-    , processGroup002              -- JS: missing "getppid"
-    , queryFdOption01              -- JS: unimplemented "fcntl_read/write/lock"
-    , signals001                   -- JS: missing "sigismember"
-    , t1185                        -- JS: missing "pipe"
-    , t3816                        -- JS: missing "setgrent"
-    , user001                      -- JS: missing "getuid"
-    , posix002                     -- JS: missing "pipe"
-    , posix005                     -- JS: missing "environ"
-    , posix006                     -- JS: missing "time"
-    , posix010                     -- JS: missing "sysconf"
-    ]
-#endif
-  , testWithFilePath
-  ]
-
-executeFile001 :: TestTree
-executeFile001 = testCase "executeFile001" $ do
-  actual <- captureStdout $
-    executeFile "echo" True ["arg1", "ar   g2"] Nothing
-  actual @?= "arg1 ar   g2\n"
-
-fileExist01 :: TestTree
-fileExist01 = testCase "fileExist01" $ do
-  fileExist "."
-    @? "file should exist"
-  not <$> fileExist "does not exist"
-    @? "file should not exist"
-
-fileStatus :: TestTree
-fileStatus = testCase "fileStatus" FileStatus.main
-
-fileStatusByteString :: TestTree
-fileStatusByteString = testCase "fileStatusByteString" FileStatusByteString.main
-
-getEnvironment01 :: TestTree
-getEnvironment01 = testCase "getEnvironment01" $ do
-  env <- getEnvironment
-  not (null env)
-    @? "environment should be non-empty"
-
-protectEnvironment :: IO a -> IO a
-protectEnvironment action = E.bracket ByteString.getEnvironment ByteString.setEnvironment $ \ _ -> action
-
-testSystemPosixEnvByteString :: TestTree
-testSystemPosixEnvByteString =
-  testGroup "System.Posix.Env.ByteString" [
-    testGroup "getEnvironment" [
-      testCase "returns the environment" $ do
-        env <- ByteString.getEnvironment
-        not (null env)
-          @? "environment should be non-empty"
-    ]
-  , testGroup "clearEnv" [
-      testCase "clears the environment" $ protectEnvironment $ do
-        ByteString.clearEnv
-        ByteString.getEnvironment >>= (@?= [])
-    ]
-  ]
-
-getGroupEntry :: TestTree
-getGroupEntry = testCase "getGroupEntry" $ do
-  let act = False <$ getGroupEntryForName "thisIsNotMeantToExist"
-  act `E.catch` (\(_ :: E.SomeException) -> return True)
-    @? "group should not exist"
-
-getUserEntry :: TestTree
-getUserEntry = testCase "getUserEntry" $ do
-  let act = False <$ getUserEntryForName "thisIsNotMeantToExist"
-  act `E.catch` (\(_ :: E.SomeException) -> return True)
-    @? "user should not exist"
-
-processGroup001 :: TestTree
-processGroup001 = testCase "processGroup001" $ do
-  pgid <- getProcessGroupID
-  pgid' <- getProcessGroupIDOf =<< getProcessID
-  pgid @?= pgid'
-
-processGroup002 :: TestTree
-processGroup002 = testCase "processGroup002" $ do
-  pid <- getProcessID
-  ppid <- getParentProcessID
-  ppgid <- getProcessGroupIDOf ppid
-  -- join the parent process
-  joinProcessGroup ppgid
-  pgid1 <- getProcessGroupID
-  ppgid @?= pgid1
-  -- be a leader
-  _ <- createProcessGroupFor pid
-  pgid2 <- getProcessGroupID
-  pid @?= fromIntegral pgid2
-  -- and join the parent again
-  setProcessGroupIDOf pid ppgid
-  pgid3 <- getProcessGroupID
-  ppgid @?= pgid3
-
-queryFdOption01 :: TestTree
-queryFdOption01 = testCase "queryFdOption01" $ do
-  not <$> queryFdOption stdOutput NonBlockingRead
-    @? "should be blocking"
-  setFdOption stdOutput NonBlockingRead True
-  queryFdOption stdOutput NonBlockingRead
-    @? "should be non-blocking"
-
-signals001 :: TestTree
-signals001 = testCase "signals001" Signals001.main
-
-t1185 :: TestTree
-t1185 = testCase "T1185" $ do
-  (stdinr, stdinw) <- createPipe
-  (stdoutr, stdoutw) <- createPipe
-  pid <- forkProcess $ do
-    hw <- fdToHandle stdoutw
-    hr <- fdToHandle stdinr
-    closeFd stdinw
-    hGetContents hr >>= hPutStr hw
-    hClose hr
-    hClose hw
-    exitImmediately ExitSuccess
-  threadDelay 100000
-  closeFd stdoutw
-  closeFd stdinw
-  hr2 <- fdToHandle stdoutr
-  hGetContents hr2 >>= putStr
-  actual <- getProcessStatus True False pid
-  actual @?= Just (Exited ExitSuccess)
-
-t3816 :: TestTree
-t3816 = testCase "T3816" $ do
-  not . null <$> getAllGroupEntries
-    @? "should be non-empty"
-  not . null <$> getAllGroupEntries
-    @? "should be non-empty"
-
-user001 :: TestTree
-user001 = testCase "user001" $ do
-  let force act = do
-        x <- act
-        x @?= x
-  force getRealUserID
-  force getRealUserID
-  force getRealGroupID
-  force getEffectiveUserID
-  force getEffectiveGroupID
-  force getGroups
-  force getEffectiveUserName
-  force $ getRealGroupID >>= getGroupEntryForID
-  force $ getRealGroupID >>= getGroupEntryForID >>= getGroupEntryForName . groupName
-  force getAllGroupEntries
-  force $ getRealUserID >>= getUserEntryForID
-  force getAllUserEntries
-
-posix002 :: TestTree
-posix002 = testCase "posix002" $ do
-  actual <- captureStdout $
-    executeFile "printenv" True [] (Just [("ONE","1"),("TWO","2")])
-  sort (lines actual) @?= ["ONE=1", "TWO=2"]
-
-posix005 :: TestTree
-posix005 = testCase "posix005" $ protectEnvironment $ do
-    hSetBuffering stdout NoBuffering
-
-    setEnvironment [("one","1"),("two","2")]
-    env0 <- getEnvironment
-    sort env0 @?= [("one","1"),("two","2")]
-
-    setEnv "foo" "bar" True
-    env1 <- getEnvironment
-    sort env1 @?= [("foo","bar"),("one","1"),("two","2")]
-
-    setEnv "foo" "baz" True
-    env2 <- getEnvironment
-    sort env2 @?= [("foo","baz"),("one","1"),("two","2")]
-
-    setEnv "fu" "bar" True
-    env3 <- getEnvironment
-    sort env3 @?= [("foo","baz"),("fu","bar"),("one","1"),("two","2")]
-
-    unsetEnv "foo"
-    env4 <- getEnvironment
-    sort env4 @?= [("fu","bar"),("one","1"),("two","2")]
-
-    clearEnv
-    env5 <- getEnvironment
-    sort env5 @?= []
-
-posix006 :: TestTree
-posix006 = testCase "posix006" $ do
-  start <- epochTime
-  blockSignals reservedSignals -- see #4504
-  _ <- sleep 1
-  finish <- epochTime
-  let slept = finish - start
-  (slept >= 1 && slept <= 2)
-    @? "should have slept between 1 and 2"
-
-posix010 :: TestTree
-posix010 = testCase "posix010" $ do
-  root <- getUserEntryForName "root"
-  userName root    @?= "root"
-  userID root      @?= 0
-  userGroupID root @?= 0
-
-  root' <- getUserEntryForID (userID root)
-  userName root'    @?= "root"
-  userID root'      @?= 0
-  userGroupID root' @?= 0
-
-  homeDirectory root @?= homeDirectory root'
-
-testWithFilePath :: TestTree
-testWithFilePath =
-  adjustOption (\(QuickCheckTests n) -> QuickCheckTests (n `max` 10000)) $
-    testGroup "withFilePath"
-      [ testProperty "ByteString" $
-        \xs -> let ys = B.pack $ filter (/= 0) xs in
-          ioProperty $ BSFP.withFilePath ys
-            (\ptr -> (=== ys) <$> B.packCString ptr)
-      , testProperty "PosixPath" $
-        \xs -> let ys = Sh.pack $ filter (/= 0) xs in
-          ioProperty $ PPFP.withFilePath (PosixString ys)
-            (\ptr -> (=== ys) <$> Sh.packCString ptr)
-      ]
-
--------------------------------------------------------------------------------
--- Utils
-
-captureStdout :: IO () -> IO String
-captureStdout = captureFd stdOutput
-
-captureFd :: Fd -> IO () -> IO String
-captureFd fd act = do
-  (dRead, dWrite) <- createPipe
-  _ <- forkProcess $ do
-    _ <- dupTo dWrite fd
-    act
-  closeFd dWrite
-  handle <- fdToHandle dRead
-  hGetContents handle
diff --git a/tests/Test.hsc b/tests/Test.hsc
new file mode 100644
--- /dev/null
+++ b/tests/Test.hsc
@@ -0,0 +1,292 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE CPP #-}
+
+{-# OPTIONS_GHC -fno-warn-deprecations #-}
+{-# OPTIONS_GHC -fno-warn-unused-imports #-}
+
+#include "HsUnix.h"
+
+module Main (main) where
+
+import Control.Applicative
+import Control.Concurrent
+import qualified Control.Exception as E
+import Control.Monad
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Short as Sh
+import Data.List (sort)
+import System.Exit
+import System.IO
+import System.OsString.Internal.Types
+import System.Posix
+import qualified System.Posix.Env.ByteString as ByteString
+import qualified System.Posix.ByteString.FilePath as BSFP
+import qualified System.Posix.PosixPath.FilePath as PPFP
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import qualified FileStatus
+import qualified FileExtendedStatus
+import qualified FileStatusByteString
+import qualified Signals001
+
+main :: IO ()
+main = defaultMain $ testGroup "All"
+  [ testGroup "Common" -- common to JS and native platforms
+    [ fileExist01
+    ]
+#if !defined(javascript_HOST_ARCH)
+  , testGroup "Native"
+    [ executeFile001               -- JS: missing "pipe"
+    , fileStatus                   -- JS: missing "openat"
+#ifdef HAVE_STATX
+    , fileExtendedStatus           -- JS: missing "openat"
+#endif
+    , fileStatusByteString         -- JS: missing "openat"
+    , getEnvironment01             -- JS: missing "environ"
+    , testSystemPosixEnvByteString -- JS: missing "environ"
+    , getGroupEntry                -- JS: missing "sysconf"
+    , getUserEntry                 -- JS: missing "sysconf"
+    , processGroup001              -- JS: missing "getpgrp"
+    , processGroup002              -- JS: missing "getppid"
+    , queryFdOption01              -- JS: unimplemented "fcntl_read/write/lock"
+    , signals001                   -- JS: missing "sigismember"
+    , t1185                        -- JS: missing "pipe"
+    , t3816                        -- JS: missing "setgrent"
+    , user001                      -- JS: missing "getuid"
+    , posix002                     -- JS: missing "pipe"
+    , posix005                     -- JS: missing "environ"
+    , posix006                     -- JS: missing "time"
+    , posix010                     -- JS: missing "sysconf"
+    ]
+#endif
+  , testWithFilePath
+  ]
+
+executeFile001 :: TestTree
+executeFile001 = testCase "executeFile001" $ do
+  actual <- captureStdout $
+    executeFile "echo" True ["arg1", "ar   g2"] Nothing
+  actual @?= "arg1 ar   g2\n"
+
+fileExist01 :: TestTree
+fileExist01 = testCase "fileExist01" $ do
+  fileExist "."
+    @? "file should exist"
+  not <$> fileExist "does not exist"
+    @? "file should not exist"
+
+fileStatus :: TestTree
+fileStatus = testCase "fileStatus" FileStatus.main
+
+fileExtendedStatus :: TestTree
+fileExtendedStatus = testCase "fileExtendedStatus" FileExtendedStatus.main
+
+fileStatusByteString :: TestTree
+fileStatusByteString = testCase "fileStatusByteString" FileStatusByteString.main
+
+getEnvironment01 :: TestTree
+getEnvironment01 = testCase "getEnvironment01" $ do
+  env <- getEnvironment
+  not (null env)
+    @? "environment should be non-empty"
+
+protectEnvironment :: IO a -> IO a
+protectEnvironment action = E.bracket ByteString.getEnvironment ByteString.setEnvironment $ \ _ -> action
+
+testSystemPosixEnvByteString :: TestTree
+testSystemPosixEnvByteString =
+  testGroup "System.Posix.Env.ByteString" [
+    testGroup "getEnvironment" [
+      testCase "returns the environment" $ do
+        env <- ByteString.getEnvironment
+        not (null env)
+          @? "environment should be non-empty"
+    ]
+  , testGroup "clearEnv" [
+      testCase "clears the environment" $ protectEnvironment $ do
+        ByteString.clearEnv
+        ByteString.getEnvironment >>= (@?= [])
+    ]
+  ]
+
+getGroupEntry :: TestTree
+getGroupEntry = testCase "getGroupEntry" $ do
+  let act = False <$ getGroupEntryForName "thisIsNotMeantToExist"
+  act `E.catch` (\(_ :: E.SomeException) -> return True)
+    @? "group should not exist"
+
+getUserEntry :: TestTree
+getUserEntry = testCase "getUserEntry" $ do
+  let act = False <$ getUserEntryForName "thisIsNotMeantToExist"
+  act `E.catch` (\(_ :: E.SomeException) -> return True)
+    @? "user should not exist"
+
+processGroup001 :: TestTree
+processGroup001 = testCase "processGroup001" $ do
+  pgid <- getProcessGroupID
+  pgid' <- getProcessGroupIDOf =<< getProcessID
+  pgid @?= pgid'
+
+processGroup002 :: TestTree
+processGroup002 = testCase "processGroup002" $ do
+  pid <- getProcessID
+  ppid <- getParentProcessID
+  ppgid <- getProcessGroupIDOf ppid
+  -- join the parent process
+  joinProcessGroup ppgid
+  pgid1 <- getProcessGroupID
+  ppgid @?= pgid1
+  -- be a leader
+  _ <- createProcessGroupFor pid
+  pgid2 <- getProcessGroupID
+  pid @?= fromIntegral pgid2
+  -- and join the parent again
+  setProcessGroupIDOf pid ppgid
+  pgid3 <- getProcessGroupID
+  ppgid @?= pgid3
+
+queryFdOption01 :: TestTree
+queryFdOption01 = testCase "queryFdOption01" $ do
+  not <$> queryFdOption stdOutput NonBlockingRead
+    @? "should be blocking"
+  setFdOption stdOutput NonBlockingRead True
+  queryFdOption stdOutput NonBlockingRead
+    @? "should be non-blocking"
+
+signals001 :: TestTree
+signals001 = testCase "signals001" Signals001.main
+
+t1185 :: TestTree
+t1185 = testCase "T1185" $ do
+  (stdinr, stdinw) <- createPipe
+  (stdoutr, stdoutw) <- createPipe
+  pid <- forkProcess $ do
+    hw <- fdToHandle stdoutw
+    hr <- fdToHandle stdinr
+    closeFd stdinw
+    hGetContents hr >>= hPutStr hw
+    hClose hr
+    hClose hw
+    exitImmediately ExitSuccess
+  threadDelay 100000
+  closeFd stdoutw
+  closeFd stdinw
+  hr2 <- fdToHandle stdoutr
+  hGetContents hr2 >>= putStr
+  actual <- getProcessStatus True False pid
+  actual @?= Just (Exited ExitSuccess)
+
+t3816 :: TestTree
+t3816 = testCase "T3816" $ do
+  not . null <$> getAllGroupEntries
+    @? "should be non-empty"
+  not . null <$> getAllGroupEntries
+    @? "should be non-empty"
+
+user001 :: TestTree
+user001 = testCase "user001" $ do
+  let force act = do
+        x <- act
+        x @?= x
+  force getRealUserID
+  force getRealUserID
+  force getRealGroupID
+  force getEffectiveUserID
+  force getEffectiveGroupID
+  force getGroups
+  force getEffectiveUserName
+  force $ getRealGroupID >>= getGroupEntryForID
+  force $ getRealGroupID >>= getGroupEntryForID >>= getGroupEntryForName . groupName
+  force getAllGroupEntries
+  force $ getRealUserID >>= getUserEntryForID
+  force getAllUserEntries
+
+posix002 :: TestTree
+posix002 = testCase "posix002" $ do
+  actual <- captureStdout $
+    executeFile "printenv" True [] (Just [("ONE","1"),("TWO","2")])
+  sort (lines actual) @?= ["ONE=1", "TWO=2"]
+
+posix005 :: TestTree
+posix005 = testCase "posix005" $ protectEnvironment $ do
+    hSetBuffering stdout NoBuffering
+
+    setEnvironment [("one","1"),("two","2")]
+    env0 <- getEnvironment
+    sort env0 @?= [("one","1"),("two","2")]
+
+    setEnv "foo" "bar" True
+    env1 <- getEnvironment
+    sort env1 @?= [("foo","bar"),("one","1"),("two","2")]
+
+    setEnv "foo" "baz" True
+    env2 <- getEnvironment
+    sort env2 @?= [("foo","baz"),("one","1"),("two","2")]
+
+    setEnv "fu" "bar" True
+    env3 <- getEnvironment
+    sort env3 @?= [("foo","baz"),("fu","bar"),("one","1"),("two","2")]
+
+    unsetEnv "foo"
+    env4 <- getEnvironment
+    sort env4 @?= [("fu","bar"),("one","1"),("two","2")]
+
+    clearEnv
+    env5 <- getEnvironment
+    sort env5 @?= []
+
+posix006 :: TestTree
+posix006 = testCase "posix006" $ do
+  start <- epochTime
+  blockSignals reservedSignals -- see #4504
+  _ <- sleep 1
+  finish <- epochTime
+  let slept = finish - start
+  (slept >= 1 && slept <= 2)
+    @? "should have slept between 1 and 2"
+
+posix010 :: TestTree
+posix010 = testCase "posix010" $ do
+  root <- getUserEntryForName "root"
+  userName root    @?= "root"
+  userID root      @?= 0
+  userGroupID root @?= 0
+
+  root' <- getUserEntryForID (userID root)
+  userName root'    @?= "root"
+  userID root'      @?= 0
+  userGroupID root' @?= 0
+
+  homeDirectory root @?= homeDirectory root'
+
+testWithFilePath :: TestTree
+testWithFilePath =
+  adjustOption (\(QuickCheckTests n) -> QuickCheckTests (n `max` 10000)) $
+    testGroup "withFilePath"
+      [ testProperty "ByteString" $
+        \xs -> let ys = B.pack $ filter (/= 0) xs in
+          ioProperty $ BSFP.withFilePath ys
+            (\ptr -> (=== ys) <$> B.packCString ptr)
+      , testProperty "PosixPath" $
+        \xs -> let ys = Sh.pack $ filter (/= 0) xs in
+          ioProperty $ PPFP.withFilePath (PosixString ys)
+            (\ptr -> (=== ys) <$> Sh.packCString ptr)
+      ]
+
+-------------------------------------------------------------------------------
+-- Utils
+
+captureStdout :: IO () -> IO String
+captureStdout = captureFd stdOutput
+
+captureFd :: Fd -> IO () -> IO String
+captureFd fd act = do
+  (dRead, dWrite) <- createPipe
+  _ <- forkProcess $ do
+    _ <- dupTo dWrite fd
+    act
+  closeFd dWrite
+  handle <- fdToHandle dRead
+  hGetContents handle
diff --git a/unix.cabal b/unix.cabal
--- a/unix.cabal
+++ b/unix.cabal
@@ -1,6 +1,6 @@
 cabal-version:  1.12
 name:           unix
-version:        2.8.2.1
+version:        2.8.3.0
 -- NOTE: Don't forget to update ./changelog.md
 
 license:        BSD3
@@ -11,13 +11,13 @@
 synopsis:       POSIX functionality
 category:       System
 build-type:     Configure
-tested-with:    GHC==9.2.4,
+tested-with:    GHC==9.6.2,
+                GHC==9.4.7,
+                GHC==9.2.8,
                 GHC==9.0.2,
                 GHC==8.10.7,
                 GHC==8.8.4,
-                GHC==8.6.5,
-                GHC==8.4.4,
-                GHC==8.2.2
+                GHC==8.6.5
 description:
     This package gives you access to the set of operating system
     services standardised by
@@ -69,10 +69,10 @@
         buildable: False
 
     build-depends:
-        base        >= 4.10    && < 4.20,
-        bytestring  >= 0.9.2   && < 0.13,
+        base        >= 4.12.0.0  && < 4.20,
+        bytestring  >= 0.9.2     && < 0.13,
         filepath    >= 1.4.100.0 && < 1.5,
-        time        >= 1.2     && < 1.13
+        time        >= 1.9.1     && < 1.13
 
     exposed-modules:
         System.Posix
@@ -167,6 +167,7 @@
     main-is: Test.hs
     other-modules:
         FileStatus
+        FileExtendedStatus
         FileStatusByteString
         Signals001
     type: exitcode-stdio-1.0
