Re: [PATCH v4] ceph: fix OOB read in ceph_osdc_list_watchers via uncapped outdata_len
Ilya Dryomov <[email protected]>
| Newsgroups | org.kernel.vger.stable,org.kernel.vger.ceph-devel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <CAOi1vP-UKO91iPY-3uNspJ5W7bYowqCdg5_GiJnOLWo8f3Q0eQ@mail.gmail.com> |
On Tue, Jul 7, 2026 at 12:58 PM Pavitra Jha <[email protected]> wrote: > > The OSD reply header field op->payload_len is wire-controlled and is > copied directly into m->outdata_len[i] without any bounds check: > > m->outdata_len[i] = le32_to_cpu(op->payload_len); > > This value propagates unchecked to req->r_ops[0].outdata_len and is Hi Pavitra, IIRC it's not completely unchecked: if the sum of m.outdata_len values for all OSD ops in the request doesn't match msg->hdr.data_len, handle_reply() should fail and hence ceph_osdc_wait_request() in ceph_osdc_list_watchers() too. > then used to set the decode boundary in ceph_osdc_list_watchers(): > > void *const end = p + req->r_ops[0].outdata_len; > > The actual data allocation is always exactly one page: > ceph_alloc_page_vector(1, GFP_NOIO) > ceph_osd_data_pages_init(..., PAGE_SIZE, ...) > > The messenger caps the copy to PAGE_SIZE bytes, but the decode window > end is set from the uncapped wire value. A malicious OSD can send I don't think the messenger ever silently caps anything (or at least it shouldn't). The messenger wants to read exactly msg->hdr.data_len bytes off the wire and if the underlying buffers aren't big enough the message gets discarded. > outdata_len=0x10000, causing _safe decoder boundary checks to pass > while the physical reads cross the slab allocation boundary. > KASAN report (kernel 7.0.0-rc7, QEMU/x86_64, KASLR disabled): Could you please elaborate on what fields were set to what values and what payload was injected and fed to decode_watchers() because it for sure wasn't just outdata_len set to 0x10000. Thanks, Ilya > > ================================================================== > BUG: KASAN: slab-out-of-bounds in ceph_oob2_init+0x23d/0xff0 [ceph_oob2_poc] > Read of size 4 at addr ffff88800a229f9e by task insmod/57 > > CPU: 0 UID: 0 PID: 57 Comm: insmod Tainted: G O 7.0.0-rc7-g9c2abf69da83-dirty #15 PREEMPT(lazy) > Tainted: [O]=OOT_MODULE > Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 > Call Trace: > <TASK> > dump_stack_lvl+0x4d/0x70 > print_report+0x170/0x4f3 > kasan_report+0xda/0x110 > ceph_oob2_init+0x23d/0xff0 [ceph_oob2_poc] > do_one_initcall+0x9a/0x3a0 > do_init_module+0x27c/0x790 > load_module+0x4a9a/0x6350 > init_module_from_file+0x15c/0x180 > idempotent_init_module+0x21f/0x750 > __x64_sys_finit_module+0xba/0x120 > do_syscall_64+0xe2/0x570 > entry_SYSCALL_64_after_hwframe+0x77/0x7f > > Allocated by task 57: > kasan_save_stack+0x30/0x50 > kasan_save_track+0x14/0x30 > __kasan_kmalloc+0x7f/0x90 > ceph_oob2_init+0x44/0xff0 [ceph_oob2_poc] > do_one_initcall+0x9a/0x3a0 > do_init_module+0x27c/0x790 > load_module+0x4a9a/0x6350 > init_module_from_file+0x15c/0x180 > idempotent_init_module+0x21f/0x750 > __x64_sys_finit_module+0xba/0x120 > do_syscall_64+0xe2/0x570 > entry_SYSCALL_64_after_hwframe+0x77/0x7f > > The buggy address belongs to the object at ffff88800a229000 > which belongs to the cache kmalloc-4k of size 4096 > The buggy address is located 3998 bytes inside of > allocated 4000-byte region [ffff88800a229000, ffff88800a229fa0) > > Memory state around the buggy address: > ffff88800a229e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 > ffff88800a229f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 > >ffff88800a229f80: 00 00 00 00 fc fc fc fc fc fc fc fc fc fc fc fc > ^ > ffff88800a22a000: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc > ffff88800a22a080: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc > ================================================================== > > val=0xccccaaaa (OOB garbage from KASAN redzone) > > Fix by introducing buf_len to hold the allocation size, using it in > both ceph_osd_data_pages_init() and the min_t() decode boundary cap, > so the two are guaranteed to stay in sync if the buffer size changes. > buf_len is declared as u32 to match the type of outdata_len used in > the min_t() expression. > > Attacker model: a malicious or compromised OSD in a multi-tenant > Ceph deployment can trigger this against any client issuing > CEPH_OSD_OP_LIST_WATCHERS without further privileges beyond OSD > session establishment. > > Fixes: a4ed38d7a180 ("libceph: support for CEPH_OSD_OP_LIST_WATCHERS") > Cc: [email protected] > Reviewed-by: Viacheslav Dubeyko <[email protected]> > Signed-off-by: Pavitra Jha <[email protected]> > --- > v4: Rebase against current linux/master and ceph-testing/testing, > confirmed clean git am -3 apply against both. No functional > changes from Slava's reviewed v3. > v3: Change buf_len type from size_t to u32 to match outdata_len type > in min_t(), per Viacheslav Dubeyko's review. > v2: Introduce buf_len variable instead of hardcoding PAGE_SIZE > independently in ceph_osd_data_pages_init() and the min_t() cap, > per Viacheslav Dubeyko's review. > --- > net/ceph/osd_client.c | 6 ++++-- > 1 file changed, 4 insertions(+), 2 deletions(-) > > diff --git a/net/ceph/osd_client.c b/net/ceph/osd_client.c > index 2ff00070c..62bec6ad8 100644 > --- a/net/ceph/osd_client.c > +++ b/net/ceph/osd_client.c > @@ -5060,6 +5060,7 @@ int ceph_osdc_list_watchers(struct ceph_osd_client *osdc, > struct ceph_osd_request *req; > struct page **pages; > int ret; > + const u32 buf_len = PAGE_SIZE; > > req = ceph_osdc_alloc_request(osdc, NULL, 1, false, GFP_NOIO); > if (!req) > @@ -5078,7 +5079,7 @@ int ceph_osdc_list_watchers(struct ceph_osd_client *osdc, > osd_req_op_init(req, 0, CEPH_OSD_OP_LIST_WATCHERS, 0); > ceph_osd_data_pages_init(osd_req_op_data(req, 0, list_watchers, > response_data), > - pages, PAGE_SIZE, 0, false, true); > + pages, buf_len, 0, false, true); > > ret = ceph_osdc_alloc_messages(req, GFP_NOIO); > if (ret) > @@ -5088,7 +5089,8 @@ int ceph_osdc_list_watchers(struct ceph_osd_client *osdc, > ret = ceph_osdc_wait_request(osdc, req); > if (ret >= 0) { > void *p = page_address(pages[0]); > - void *const end = p + req->r_ops[0].outdata_len; > + void *const end = p + > + min_t(u32, req->r_ops[0].outdata_len, buf_len); > > ret = decode_watchers(&p, end, watchers, num_watchers); > } > -- > 2.53.0 >