Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use-after-free in arc header from dbuf code in ztest #15293

Closed
pcd1193182 opened this issue Sep 18, 2023 · 5 comments
Closed

Use-after-free in arc header from dbuf code in ztest #15293

pcd1193182 opened this issue Sep 18, 2023 · 5 comments
Labels
Component: Encryption "native encryption" feature Component: Memory Management kernel memory management Type: Defect Incorrect behavior (e.g. crash, hang)

Comments

@pcd1193182
Copy link
Contributor

I got this report from a ztest run with ASAN enabled:

==14240==ERROR: AddressSanitizer: heap-use-after-free on address 0x6140008146d8 at pc 0x7f071297aacd bp 0x7f06bbe16670 s
p 0x7f06bbe16660
READ of size 8 at 0x6140008146d8 thread T313
  1.60 sec in ztest_dmu_object_alloc_free
Executing ztest_zil_commit
    #0 0x7f071297aacc in arc_buf_access ../module/zfs/arc.c:5368
    #1 0x7f07129b9a26 in dbuf_hold_impl ../module/zfs/dbuf.c:3734
    #2 0x7f07129bbe37 in dbuf_hold_level ../module/zfs/dbuf.c:3801
    #3 0x7f07129d3c62 in dmu_buf_hold_noread ../module/zfs/dmu.c:203
    #4 0x7f07129d3ec1 in dmu_buf_hold ../module/zfs/dmu.c:253
    #5 0x558881763b1c in ztest_get_data cmd/ztest.c:2581
    #6 0x7f0712c87a57 in zil_lwb_commit ../module/zfs/zil.c:2188
    #7 0x7f0712c907bd in zil_lwb_write_issue ../module/zfs/zil.c:1804
    #8 0x7f0712c9250a in zil_commit_waiter_timeout ../module/zfs/zil.c:3135
    #9 0x7f0712c9298a in zil_commit_waiter ../module/zfs/zil.c:3222
    #10 0x7f0712c9647f in zil_commit_impl ../module/zfs/zil.c:3528
    #11 0x55888176459d in ztest_zil_commit cmd/ztest.c:2991
    #12 0x5588817606e9 in ztest_execute cmd/ztest.c:7391
    #13 0x5588817676fd in ztest_thread cmd/ztest.c:7438
    #14 0x7f07128e19ee in zk_thread_wrapper libzpool/kernel.c:90
    #15 0x7f070ed90608 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x8608)
    #16 0x7f070ecb5132 in __clone (/lib/x86_64-linux-gnu/libc.so.6+0x11f132)

0x6140008146d8 is located 152 bytes inside of 432-byte region [0x614000814640,0x6140008147f0)
freed by thread T112 here:
    #0 0x7f071333240f in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:122
    #1 0x7f0712965a63 in arc_hdr_realloc_crypt ../module/zfs/arc.c:3576
    #2 0x7f07129732af in arc_write_ready ../module/zfs/arc.c:6586
    #3 0x7f0712cb6626 in zio_ready ../module/zfs/zio.c:4728
    #4 0x7f0712c9c489 in __zio_execute ../module/zfs/zio.c:2382
    #5 0x7f0712c9c489 in zio_execute ../module/zfs/zio.c:2293
    #6 0x7f07128e5cbf in taskq_thread libzpool/taskq.c:240
    #7 0x7f07128e19ee in zk_thread_wrapper libzpool/kernel.c:90
    #8 0x7f070ed90608 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x8608)
    
    previously allocated by thread T313 here:
    #0 0x7f0713332808 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:144
    #1 0x7f071295b914 in umem_alloc ../lib/libspl/include/umem.h:93
    #2 0x7f071295f739 in umem_cache_alloc ../lib/libspl/include/umem.h:201
    #3 0x7f071296426b in arc_hdr_alloc ../module/zfs/arc.c:3311
    #4 0x7f07129772a2 in arc_alloc_buf ../module/zfs/arc.c:3625
    #5 0x7f07129aee54 in dbuf_read_hole ../module/zfs/dbuf.c:1470
    #6 0x7f07129b48c4 in dbuf_read_impl ../module/zfs/dbuf.c:1594
    #7 0x7f07129b5729 in dbuf_read ../module/zfs/dbuf.c:1817
    #8 0x7f07129bfe03 in dmu_buf_will_dirty_impl ../module/zfs/dbuf.c:2664
    #9 0x7f07129d2714 in dmu_write_impl ../module/zfs/dmu.c:1139
    #10 0x7f07129d6ca8 in dmu_write ../module/zfs/dmu.c:1164
    #11 0x558881773b07 in ztest_replay_write cmd/ztest.c:2338
    #12 0x5588817743f0 in ztest_write cmd/ztest.c:2774
    #13 0x5588817753e9 in ztest_io cmd/ztest.c:2878
    #14 0x5588817775ef in ztest_dmu_write_parallel cmd/ztest.c:5388
    #15 0x5588817606e9 in ztest_execute cmd/ztest.c:7391
    #16 0x5588817676fd in ztest_thread cmd/ztest.c:7438
    #17 0x7f07128e19ee in zk_thread_wrapper libzpool/kernel.c:90
    #18 0x7f070ed90608 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x8608)

From my reading of the code, what happened here is fairly obvious; we have a dbuf with an arc buf storing its data. That arc buffer has dirty data in it, and we’re in the process of writing it out. Part of that is transforming the arc buffer into a protected buffer (because encryption is enabled in the dataset). When we do that, we replace the header with a new one, and then free it. Unfortunately, there is no synchronization happening here; any references to the existing header will remain outstanding. As a result, it’s possible to UAF the headers, with bad timing. This seems like a fairly obvious race, but there is no synchronization mechanism here. I believe fixing it would involve introducing a mutex that you have to hold around access to the arc header from the arc buf.

@pcd1193182 pcd1193182 added Type: Defect Incorrect behavior (e.g. crash, hang) Component: Memory Management kernel memory management labels Sep 18, 2023
@rincebrain rincebrain added the Component: Encryption "native encryption" feature label Sep 19, 2023
@amotin
Copy link
Member

amotin commented Sep 20, 2023

I had similar question about headers reallocation into crypto during my latest ARC tour. I haven't seen there any locking either. But since it does not explode all the time I guessed it may be at least partially protected by the header/buffer life cycle. Worth another look.

Meanwhile looking on this specific panic I've got orthogonal question: should we really call arc_buf_access() when getting data for ZIL write? We do not want MFU promotions just from the fact the write was synchronous.

@amotin
Copy link
Member

amotin commented Sep 20, 2023

Actually, thinking more of it, I recall b_evict_lock I dropped in 2.2 branch, since it was not protecting anything any more. It was used in arc_hdr_realloc_crypt() in such a chunk, that in my opinion did not really protect from the use-after-free:

        for (buf = nhdr->b_l1hdr.b_buf; buf != NULL; buf = buf->b_next) {
                mutex_enter(&buf->b_evict_lock);
                buf->b_hdr = nhdr;
                mutex_exit(&buf->b_evict_lock);
        }

@rincebrain
Copy link
Contributor

I told pcd this OOB, but this really just seems like #11679 to me from the trace.

@amotin
Copy link
Member

amotin commented Sep 28, 2023

Looking more into this, I think b_evict_lock did protect from certain races around arc_hdr_realloc_crypt(), being taken inside arc_buf_access() and arc_release(). I just doubt it covered all the cases. For example calls like arc_referenced() were racy, even though with extremely small windows, or arc_release() read buf->b_hdr before taking b_evict_lock.

I am not exactly happy to resurrect that additional lock in pretty hot paths. I am thinking about moving arc_hdr_realloc_crypt() from arc_write_ready() into higher level awcb_ready callbacks under db_mtx. I think it should be both safer and cheaper, just may be a bit less obvious.

amotin added a commit to amotin/zfs that referenced this issue Sep 29, 2023
As part of openzfs#14340 I've removed b_evict_lock, that played no role in
ARC eviction process.  But appears it played a secondary role of
protecting b_hdr pointers in arc_buf_t during header reallocation
by arc_hdr_realloc_crypt(), that, as found in openzfs#15293, may cause use
after free races if some encrypted block is read while being synced
after been writen.

After closer look on b_evict_lock I still do not believe it covered
all the possible races, so I am not eager to resurrect it.  Instead
this refactors arc_hdr_realloc_crypt() into arc_realloc_crypt()
and moves its calls from arc_write_ready() into upper levels ready
callbacks, like dbuf_write_ready(), where it is protected by
existing db_mtx, protecting also all the arc buffer accesses.

Signed-off-by:	Alexander Motin <[email protected]>
Sponsored by:	iXsystems, Inc.
amotin added a commit to amotin/zfs that referenced this issue Oct 3, 2023
To reduce memory usage ZFS crypto allocated bigger by 64 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 64 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 40 bytes with total header
size of 240 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Signed-off-by:	Alexander Motin <[email protected]>
Sponsored by:	iXsystems, Inc.
@amotin
Copy link
Member

amotin commented Oct 3, 2023

I gave up on attempt to use db_mtx lock. Instead in #15347 I decided to just drop the header reallocation. With growing ashifts and block sizes extra 32 bytes may be not so high price for simplicity these days.

amotin added a commit to amotin/zfs that referenced this issue Oct 4, 2023
To reduce memory usage ZFS crypto allocated bigger by 64 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 64 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 40 bytes with total header
size of 240 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Signed-off-by:	Alexander Motin <[email protected]>
Sponsored by:	iXsystems, Inc.
amotin added a commit to amotin/zfs that referenced this issue Oct 4, 2023
To reduce memory usage ZFS crypto allocated bigger by 64 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 64 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 40 bytes with total header
size of 240 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Signed-off-by:	Alexander Motin <[email protected]>
Sponsored by:	iXsystems, Inc.
amotin added a commit to amotin/zfs that referenced this issue Oct 5, 2023
To reduce memory usage ZFS crypto allocated bigger by 64 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 64 bytes per header, but with couple patches
saving 32 bytes, the net growth is only 32 bytes with total header
size of 232 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Signed-off-by:	Alexander Motin <[email protected]>
Sponsored by:	iXsystems, Inc.
amotin added a commit to amotin/zfs that referenced this issue Oct 5, 2023
To reduce memory usage ZFS crypto allocated bigger by 64 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 56 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 32 bytes with total header
size of 232 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Signed-off-by:	Alexander Motin <[email protected]>
Sponsored by:	iXsystems, Inc.
amotin added a commit to amotin/zfs that referenced this issue Oct 5, 2023
To reduce memory usage ZFS crypto allocated bigger by 56 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 56 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 32 bytes with total header
size of 232 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Signed-off-by:	Alexander Motin <[email protected]>
Sponsored by:	iXsystems, Inc.
behlendorf pushed a commit to behlendorf/zfs that referenced this issue Oct 6, 2023
To reduce memory usage ZFS crypto allocated bigger by 56 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 56 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 32 bytes with total header
size of 232 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Reviewe-by: Brian Behlendorf <[email protected]>
Signed-off-by: Alexander Motin <[email protected]>
Sponsored by: iXsystems, Inc.
Closes openzfs#15293
Closes openzfs#15347
behlendorf pushed a commit that referenced this issue Oct 7, 2023
To reduce memory usage ZFS crypto allocated bigger by 56 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of #14340.
As result, as was found in #15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 56 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 32 bytes with total header
size of 232 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Reviewe-by: Brian Behlendorf <[email protected]>
Signed-off-by: Alexander Motin <[email protected]>
Sponsored by: iXsystems, Inc.
Closes #15293
Closes #15347
lundman pushed a commit to openzfsonwindows/openzfs that referenced this issue Dec 12, 2023
To reduce memory usage ZFS crypto allocated bigger by 56 bytes ARC
headers only when specific block was encrypted on disk.  It was a
nice optimization, except in some cases the code reallocated them
on fly, that invalidated header pointers from the buffers.  Since
the buffers use different locking, it created number of races, that
were originally covered (at least partially) by b_evict_lock, used
also to protection evictions.  But it has gone as part of openzfs#14340.
As result, as was found in openzfs#15293, arc_hdr_realloc_crypt() ended
up unprotected and causing use-after-free.

Instead of introducing some even more elaborate locking, this patch
just drops the difference between normal and protected headers. It
cost us additional 56 bytes per header, but with couple patches
saving 24 bytes, the net growth is only 32 bytes with total header
size of 232 bytes on FreeBSD, that IMHO is acceptable price for
simplicity.  Additional locking would also end up consuming space,
time or both.

Reviewe-by: Brian Behlendorf <[email protected]>
Signed-off-by: Alexander Motin <[email protected]>
Sponsored by: iXsystems, Inc.
Closes openzfs#15293
Closes openzfs#15347
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Component: Encryption "native encryption" feature Component: Memory Management kernel memory management Type: Defect Incorrect behavior (e.g. crash, hang)
Projects
None yet
Development

No branches or pull requests

3 participants