aio: Make AIO more compatible with valgrind
authorAndres Freund <[email protected]>
Mon, 7 Apr 2025 19:20:30 +0000 (15:20 -0400)
committerAndres Freund <[email protected]>
Mon, 7 Apr 2025 19:20:30 +0000 (15:20 -0400)
In some edge cases valgrind flags issues with the memory referenced by
IOs. All of the cases addressed in this change are false positives.

Most of the false positives are caused by UnpinBuffer[NoOwner] marking buffer
data as inaccessible. This happens even though the AIO subsystem still holds a
pin. That's good, there shouldn't be accesses to the buffer outside of AIO
related code until it is pinned by "user" code again. But it requires some
explicit work - if the buffer is not pinned by the current backend, we need to
explicitly mark the buffer data accessible/inaccessible while executing
completion callbacks.

That however causes a cascading issue in IO workers: After the completion
callbacks for a buffer is executed, the page is marked as inaccessible. If
subsequently the same worker is executing IO targeting the same buffer, we
would get an error, as the memory is still marked inaccessible. To avoid that,
we need to explicitly mark the memory as accessible in IO workers.

Another issue is that IO executed in workers or via io_uring will not mark
memory as DEFINED. In the case of workers that is because valgrind does not
track memory definedness across processes. For io_uring that is because
valgrind does not understand io_uring, and therefore its IOs never mark memory
as defined, whether the completions are processed in the defining process or
in another context.  It's not entirely clear how to best solve that. The
current user of AIO is not affected, as it explicitly marks buffers as DEFINED
& NOACCESS anyway.  Defer solving this issue until we have a user with
different needs.

Per buildfarm animal skink.

Reviewed-by: Noah Misch <[email protected]>
Co-authored-by: Noah Misch <[email protected]>
Discussion: https://postgr.es/m/3pd4322mogfmdd5nln3zphdwhtmq3rzdldqjwb2sfqzcgs22lf@ok2gletdaoe6

src/backend/storage/aio/aio_io.c
src/backend/storage/aio/method_worker.c
src/backend/storage/buffer/bufmgr.c
src/backend/storage/smgr/smgr.c
src/include/storage/aio_internal.h

index 4d31392ddc7048c8de6791e44a92c4fd0b09ed2f..00e176135a603cd1fc1b37b31eed56d122ce2a9e 100644 (file)
@@ -210,3 +210,26 @@ pgaio_io_uses_fd(PgAioHandle *ioh, int fd)
 
    return false;               /* silence compiler */
 }
+
+/*
+ * Return the iovec and its length. Currently only expected to be used by
+ * debugging infrastructure
+ */
+int
+pgaio_io_get_iovec_length(PgAioHandle *ioh, struct iovec **iov)
+{
+   Assert(ioh->state >= PGAIO_HS_DEFINED);
+
+   *iov = &pgaio_ctl->iovecs[ioh->iovec_off];
+
+   switch (ioh->op)
+   {
+       case PGAIO_OP_READV:
+           return ioh->op_data.read.iov_length;
+       case PGAIO_OP_WRITEV:
+           return ioh->op_data.write.iov_length;
+       default:
+           pg_unreachable();
+           return 0;
+   }
+}
index 31d94ac82c54039f1d0a205b8e6743003f6d66b6..8ad17ec1ef7393f788eefd15ae68570386cc7e56 100644 (file)
@@ -42,6 +42,7 @@
 #include "storage/latch.h"
 #include "storage/proc.h"
 #include "tcop/tcopprot.h"
+#include "utils/memdebug.h"
 #include "utils/ps_status.h"
 #include "utils/wait_event.h"
 
@@ -529,6 +530,24 @@ IoWorkerMain(const void *startup_data, size_t startup_data_len)
            error_errno = 0;
            error_ioh = NULL;
 
+           /*
+            * As part of IO completion the buffer will be marked as NOACCESS,
+            * until the buffer is pinned again - which never happens in io
+            * workers. Therefore the next time there is IO for the same
+            * buffer, the memory will be considered inaccessible. To avoid
+            * that, explicitly allow access to the memory before reading data
+            * into it.
+            */
+#ifdef USE_VALGRIND
+           {
+               struct iovec *iov;
+               uint16      iov_length = pgaio_io_get_iovec_length(ioh, &iov);
+
+               for (int i = 0; i < iov_length; i++)
+                   VALGRIND_MAKE_MEM_UNDEFINED(iov[i].iov_base, iov[i].iov_len);
+           }
+#endif
+
            /*
             * We don't expect this to ever fail with ERROR or FATAL, no need
             * to keep error_ioh set to the IO.
index 5da121872f436fd45d0d0280b5dd2fbe27528c65..941d7fa3d9435b0ae5cfd9e62c1d8e626afa5458 100644 (file)
@@ -6881,6 +6881,19 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
    /* Check for garbage data. */
    if (!failed)
    {
+       /*
+        * If the buffer is not currently pinned by this backend, e.g. because
+        * we're completing this IO after an error, the buffer data will have
+        * been marked as inaccessible when the buffer was unpinned. The AIO
+        * subsystem holds a pin, but that doesn't prevent the buffer from
+        * having been marked as inaccessible. The completion might also be
+        * executed in a different process.
+        */
+#ifdef USE_VALGRIND
+       if (!BufferIsPinned(buffer))
+           VALGRIND_MAKE_MEM_DEFINED(bufdata, BLCKSZ);
+#endif
+
        if (!PageIsVerified((Page) bufdata, tag.blockNum, piv_flags,
                            failed_checksum))
        {
@@ -6899,6 +6912,12 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
        else if (*failed_checksum)
            *ignored_checksum = true;
 
+       /* undo what we did above */
+#ifdef USE_VALGRIND
+       if (!BufferIsPinned(buffer))
+           VALGRIND_MAKE_MEM_NOACCESS(bufdata, BLCKSZ);
+#endif
+
        /*
         * Immediately log a message about the invalid page, but only to the
         * server log. The reason to do so immediately is that this may be
index 4540284f5817c2e92ef4f1798d1dc0fe170f0a68..bce37a36d51ba70a8c2632387fb722e5e836c328 100644 (file)
@@ -746,6 +746,8 @@ smgrreadv(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
  *   responsible for pgaio_result_report() to mirror that news to the user (if
  *   the IO results in PGAIO_RS_WARNING) or abort the (sub)transaction (if
  *   PGAIO_RS_ERROR).
+ * - Under Valgrind, the "buffers" memory may or may not change status to
+ *   DEFINED, depending on io_method and concurrent activity.
  */
 void
 smgrstartreadv(PgAioHandle *ioh,
index 7f18da2c85651638775a555f7bc56bf8123eccb1..33f27b9fe508d1404f2cf37271982ea918efe10c 100644 (file)
@@ -344,6 +344,7 @@ extern PgAioResult pgaio_io_call_complete_local(PgAioHandle *ioh);
 extern void pgaio_io_perform_synchronously(PgAioHandle *ioh);
 extern const char *pgaio_io_get_op_name(PgAioHandle *ioh);
 extern bool pgaio_io_uses_fd(PgAioHandle *ioh, int fd);
+extern int pgaio_io_get_iovec_length(PgAioHandle *ioh, struct iovec **iov);
 
 /* aio_target.c */
 extern bool pgaio_io_can_reopen(PgAioHandle *ioh);