From 647b6be4893461f2459e1e9237cda05c29657c12 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sun, 10 May 2026 08:06:07 +0800 Subject: [PATCH] vim-patch:9.2.0461: Corrupted undofile causes use-after-free (#39707) Problem: The four pointer-resolution loops in u_read_undo() lack an i != j guard, so a header whose uh_next.seq equals its own uh_seq resolves uh_next.ptr to itself. On buffer close, u_freeheader() sees uhp->uh_next.ptr != NULL and skips updating b_u_oldhead, so u_blockfree() dereferences the freed header on the next iteration. The same pattern applies to uh_prev, uh_alt_next and uh_alt_prev. A crafted .un~ file in the same directory as a text file can trigger the use-after-free and subsequent double-free when the buffer is closed. (Daniel Cervera) Solution: Add an i != j guard to each of the four resolution loops, matching the guard already present in the duplicate-detection loop above. closes: vim/vim#20168 Supported by AI https://github.com/vim/vim/commit/4f610f07b76b4f34d011179c2531ab4517ac11e8 Co-authored-by: Christian Brabandt (cherry picked from commit 2d5f56c0aaded1d9fc903d93d55cbd07c5385ab1) --- src/nvim/undo.c | 60 ++++++++++++++++++++-------------- test/old/testdir/test_undo.vim | 39 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 960b6bd264..9810e30f21 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -1565,36 +1565,48 @@ void u_read_undo(char *name, const uint8_t *hash, const char *orig_name FUNC_ATT goto error; } } - for (int j = 0; j < num_head; j++) { - if (uhp_table[j] != NULL - && uhp_table[j]->uh_seq == uhp->uh_next.seq) { - uhp->uh_next.ptr = uhp_table[j]; - SET_FLAG(j); - break; + { + const int seq = uhp->uh_next.seq; + uhp->uh_next.ptr = NULL; + for (int j = 0; j < num_head; j++) { + if (uhp_table[j] != NULL && i != j && uhp_table[j]->uh_seq == seq) { + uhp->uh_next.ptr = uhp_table[j]; + SET_FLAG(j); + break; + } } } - for (int j = 0; j < num_head; j++) { - if (uhp_table[j] != NULL - && uhp_table[j]->uh_seq == uhp->uh_prev.seq) { - uhp->uh_prev.ptr = uhp_table[j]; - SET_FLAG(j); - break; + { + const int seq = uhp->uh_prev.seq; + uhp->uh_prev.ptr = NULL; + for (int j = 0; j < num_head; j++) { + if (uhp_table[j] != NULL && i != j && uhp_table[j]->uh_seq == seq) { + uhp->uh_prev.ptr = uhp_table[j]; + SET_FLAG(j); + break; + } } } - for (int j = 0; j < num_head; j++) { - if (uhp_table[j] != NULL - && uhp_table[j]->uh_seq == uhp->uh_alt_next.seq) { - uhp->uh_alt_next.ptr = uhp_table[j]; - SET_FLAG(j); - break; + { + const int seq = uhp->uh_alt_next.seq; + uhp->uh_alt_next.ptr = NULL; + for (int j = 0; j < num_head; j++) { + if (uhp_table[j] != NULL && i != j && uhp_table[j]->uh_seq == seq) { + uhp->uh_alt_next.ptr = uhp_table[j]; + SET_FLAG(j); + break; + } } } - for (int j = 0; j < num_head; j++) { - if (uhp_table[j] != NULL - && uhp_table[j]->uh_seq == uhp->uh_alt_prev.seq) { - uhp->uh_alt_prev.ptr = uhp_table[j]; - SET_FLAG(j); - break; + { + const int seq = uhp->uh_alt_prev.seq; + uhp->uh_alt_prev.ptr = NULL; + for (int j = 0; j < num_head; j++) { + if (uhp_table[j] != NULL && i != j && uhp_table[j]->uh_seq == seq) { + uhp->uh_alt_prev.ptr = uhp_table[j]; + SET_FLAG(j); + break; + } } } if (old_header_seq > 0 && old_idx < 0 && uhp->uh_seq == old_header_seq) { diff --git a/test/old/testdir/test_undo.vim b/test/old/testdir/test_undo.vim index fe3d5f8209..2527c43f40 100644 --- a/test/old/testdir/test_undo.vim +++ b/test/old/testdir/test_undo.vim @@ -924,4 +924,43 @@ func Test_restore_cursor_position_after_undo() endfunc +" Corrupted undo file via cyclic cross-references caused +" double free +func Test_corrupted_undofile() + CheckFeature persistent_undo + let _uf = &undofile + set undofile + new + call setline(1, 'hello') + let b=eval('0z56696D9F556E446FE50002F3AEFE62965A91903610' .. + \ 'F0E23CC8A69D5B87CEA6D28E75489B0D2CA02ED7993C' .. + \ '00000001000000000000000000000000000000010000' .. + \ '00010000000100000001000000010000000100000000' .. + \ '3B9ACA00005FD0000000010000000000000000000000' .. + \ '00000000010000000100000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '00000000000000000000000000000000000000000000' .. + \ '000000000000000000000000000000000000003B9ACA' .. + \ '00003581E7AA') + " Nvim: convert undo file format + let b = b->slice(0, -2) + 0z3581 + b->slice(-2) | let b[10] = 0x03 + call writefile(b, 'Xundo', 'bD') + rundo Xundo + bw! + let &undofile = _uf +endfunc + " vim: shiftwidth=2 sts=2 expandtab