mirror of
https://github.com/neovim/neovim.git
synced 2026-08-26 00:51:53 +00:00
fix(undo): crash on corrupted undo file #41339
Problem:
`:rundo` on a corrupted undo file crashes or hangs, instead of failing
with E825. Patching one 4-byte field is enough:
ue_size = 0xFFFFFFFF " walks a NULL ue_array
ue_size = 0x7FFFFFF0 " 17 GB xmalloc + memset, then preserve_exit()
ue_top = 0xFFFFFFFB " negative lnum reaches ml_delete()
Analysis:
Every count in the file is read with `undo_read_4c()` and then checked,
differently at each site. None bounds the value by what the file can
hold, so a 2 GB count reaches `xmalloc()`.
Note:
- Vim doesn't have `bi_fsize` because it checks `U_ALLOC_LINE` result
everywhere (thus doesn't crash, but may thrash...); those checks were
dropped when Nvim moved to `xmalloc()`, and the `ue_size` loop counter
became unsigned.
- Vim *does* have the negative line numbers bug: `u_undoredo()` checks
`top > ml_line_count || top >= bot || bot > ml_line_count + 1`, which
rejects none of them.
Solution:
- Introduce `undo_read_len()` and use it to fail early instead of
continuing with nonsense.
- Validate `ue_top`/`ue_bot`/ `ue_lcount`.
- Use `xcalloc()`, so no site can proceed with a NULL array.
- Report a truncated "U" line, distinguish EOF from a 0xFFFFFFFF field,
and free the header on the extmark error path.
This commit is contained in:
@@ -140,6 +140,7 @@
|
||||
typedef struct {
|
||||
buf_T *bi_buf;
|
||||
FILE *bi_fp;
|
||||
off_T bi_fsize; ///< Size of `bi_fp` when reading, 0 if unknown.
|
||||
} bufinfo_T;
|
||||
|
||||
#include "undo.c.generated.h"
|
||||
@@ -959,7 +960,7 @@ static u_header_T *unserialize_uhp(bufinfo_T *bi, const char *file_name)
|
||||
last_uep->ue_next = uep;
|
||||
}
|
||||
last_uep = uep;
|
||||
if (uep == NULL || error) {
|
||||
if (error) {
|
||||
u_free_uhp(uhp);
|
||||
return NULL;
|
||||
}
|
||||
@@ -977,8 +978,8 @@ static u_header_T *unserialize_uhp(bufinfo_T *bi, const char *file_name)
|
||||
bool error = false;
|
||||
ExtmarkUndoObject *extup = unserialize_extmark(bi, &error, file_name);
|
||||
if (error) {
|
||||
kv_destroy(uhp->uh_extmark);
|
||||
xfree(extup);
|
||||
u_free_uhp(uhp);
|
||||
return NULL;
|
||||
}
|
||||
kv_push(uhp->uh_extmark, *extup);
|
||||
@@ -1079,6 +1080,7 @@ static bool serialize_uep(bufinfo_T *bi, u_entry_T *uep)
|
||||
}
|
||||
|
||||
static u_entry_T *unserialize_uep(bufinfo_T *bi, bool *error, const char *file_name)
|
||||
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
|
||||
{
|
||||
u_entry_T *uep = xmalloc(sizeof(u_entry_T));
|
||||
CLEAR_POINTER(uep);
|
||||
@@ -1088,26 +1090,26 @@ static u_entry_T *unserialize_uep(bufinfo_T *bi, bool *error, const char *file_n
|
||||
uep->ue_top = undo_read_4c(bi);
|
||||
uep->ue_bot = undo_read_4c(bi);
|
||||
uep->ue_lcount = undo_read_4c(bi);
|
||||
uep->ue_size = undo_read_4c(bi);
|
||||
|
||||
char **array = NULL;
|
||||
if (uep->ue_size > 0) {
|
||||
if ((size_t)uep->ue_size < SIZE_MAX / sizeof(char *)) {
|
||||
array = xmalloc(sizeof(char *) * (size_t)uep->ue_size);
|
||||
memset(array, 0, sizeof(char *) * (size_t)uep->ue_size);
|
||||
}
|
||||
if (uep->ue_top < 0 || uep->ue_bot < 0 || uep->ue_lcount < 0) {
|
||||
// Fail early; u_undoredo() takes these as line numbers.
|
||||
corruption_error("entry lnum", file_name);
|
||||
*error = true;
|
||||
return uep;
|
||||
}
|
||||
|
||||
uep->ue_size = undo_read_len(bi, "entry size", file_name);
|
||||
if (uep->ue_size < 0) {
|
||||
uep->ue_size = 0; // u_freeentry() must not walk ue_array.
|
||||
*error = true;
|
||||
return uep;
|
||||
}
|
||||
|
||||
char **array = uep->ue_size > 0 ? xcalloc((size_t)uep->ue_size, sizeof(char *)) : NULL;
|
||||
uep->ue_array = array;
|
||||
|
||||
for (size_t i = 0; i < (size_t)uep->ue_size; i++) {
|
||||
int line_len = undo_read_4c(bi);
|
||||
char *line;
|
||||
if (line_len >= 0) {
|
||||
line = undo_read_string(bi, (size_t)line_len);
|
||||
} else {
|
||||
line = NULL;
|
||||
corruption_error("line length", file_name);
|
||||
}
|
||||
int line_len = undo_read_len(bi, "line length", file_name);
|
||||
char *line = line_len < 0 ? NULL : undo_read_string(bi, (size_t)line_len);
|
||||
if (line == NULL) {
|
||||
*error = true;
|
||||
return uep;
|
||||
@@ -1453,9 +1455,11 @@ void u_read_undo(char *name, const uint8_t *hash, const char *orig_name FUNC_ATT
|
||||
goto error;
|
||||
}
|
||||
|
||||
FileInfo file_info;
|
||||
bufinfo_T bi = {
|
||||
.bi_buf = curbuf,
|
||||
.bi_fp = fp,
|
||||
.bi_fsize = os_fileinfo_fd(fileno(fp), &file_info) ? (off_T)os_fileinfo_size(&file_info) : 0,
|
||||
};
|
||||
|
||||
// Read the undo file header.
|
||||
@@ -1492,13 +1496,17 @@ void u_read_undo(char *name, const uint8_t *hash, const char *orig_name FUNC_ATT
|
||||
}
|
||||
|
||||
// Read undo data for "U" command.
|
||||
int str_len = undo_read_4c(&bi);
|
||||
int str_len = undo_read_len(&bi, "line length", file_name);
|
||||
if (str_len < 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (str_len > 0) {
|
||||
line_ptr = undo_read_string(&bi, (size_t)str_len);
|
||||
if (line_ptr == NULL) {
|
||||
corruption_error("truncated", file_name);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
linenr_T line_lnum = (linenr_T)undo_read_4c(&bi);
|
||||
colnr_T line_colnr = (colnr_T)undo_read_4c(&bi);
|
||||
@@ -1511,7 +1519,10 @@ void u_read_undo(char *name, const uint8_t *hash, const char *orig_name FUNC_ATT
|
||||
int old_header_seq = undo_read_4c(&bi);
|
||||
int new_header_seq = undo_read_4c(&bi);
|
||||
int cur_header_seq = undo_read_4c(&bi);
|
||||
int num_head = undo_read_4c(&bi);
|
||||
int num_head = undo_read_len(&bi, "num_head", file_name);
|
||||
if (num_head < 0) {
|
||||
goto error;
|
||||
}
|
||||
int seq_last = undo_read_4c(&bi);
|
||||
int seq_cur = undo_read_4c(&bi);
|
||||
time_t seq_time = undo_read_time(&bi);
|
||||
@@ -1543,9 +1554,7 @@ void u_read_undo(char *name, const uint8_t *hash, const char *orig_name FUNC_ATT
|
||||
// sequence numbers of the headers.
|
||||
// When there are no headers uhp_table is NULL.
|
||||
if (num_head > 0) {
|
||||
if ((size_t)num_head < SIZE_MAX / sizeof(*uhp_table)) {
|
||||
uhp_table = xmalloc((size_t)num_head * sizeof(*uhp_table));
|
||||
}
|
||||
uhp_table = xcalloc((size_t)num_head, sizeof(*uhp_table));
|
||||
}
|
||||
|
||||
int num_read_uhps = 0;
|
||||
@@ -1739,6 +1748,24 @@ static int undo_read_4c(bufinfo_T *bi)
|
||||
return get4c(bi->bi_fp);
|
||||
}
|
||||
|
||||
/// Reads a 4-byte count/length field. A corrupted file can hold any value here, so reject negative
|
||||
/// or if it exceeds the bytes left in the file.
|
||||
///
|
||||
/// @param what Name of the field, for the error message.
|
||||
/// @return The value, or -1 if invalid (reported).
|
||||
static int undo_read_len(bufinfo_T *bi, const char *what, const char *file_name)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
int len = undo_read_4c(bi);
|
||||
off_T pos = vim_ftell(bi->bi_fp);
|
||||
if (len < 0 || (bi->bi_fsize > 0 && (pos < 0 || len > bi->bi_fsize - pos))) {
|
||||
// get4c() also returns -1 for a file that ends here.
|
||||
corruption_error(feof(bi->bi_fp) ? "truncated" : what, file_name);
|
||||
return -1;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static int undo_read_2c(bufinfo_T *bi)
|
||||
{
|
||||
return get2c(bi->bi_fp);
|
||||
|
||||
@@ -238,3 +238,69 @@ describe("opening file when 'undofile' is on", function()
|
||||
n.assert_alive()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('undo file', function()
|
||||
before_each(clear)
|
||||
|
||||
--- Offset of the first undo entry in `blob`: magic bytes 0xf5 0x18, followed by 4-byte ue_top,
|
||||
--- ue_bot, ue_lcount, ue_size.
|
||||
local function find_entry(blob)
|
||||
for i = 1, #blob - 18 do
|
||||
if blob:byte(i) == 0xf5 and blob:byte(i + 1) == 0x18 then
|
||||
local ok = true
|
||||
-- This expects the ue_xx values to be small, so other data (e.g. a timestamp) containing
|
||||
-- the magic bytes is not mistaken for an entry.
|
||||
for field = 0, 3 do
|
||||
local off = i + 2 + field * 4
|
||||
ok = ok
|
||||
and blob:byte(off) == 0
|
||||
and blob:byte(off + 1) == 0
|
||||
and blob:byte(off + 2) == 0
|
||||
and blob:byte(off + 3) <= 8
|
||||
end
|
||||
if ok then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If a corrupted entry is not rejected on load, it would be linked into the undo tree, then
|
||||
-- u_undoredo() acts on its line numbers and u_freeentry() walks ue_array.
|
||||
it('rejects corrupted entry (E825)', function()
|
||||
local txt = t.tmpname()
|
||||
local undo = txt .. '.undo'
|
||||
|
||||
t.write_file(txt, 'one\ntwo\n')
|
||||
command('edit ' .. txt)
|
||||
command('set undolevels=100')
|
||||
command("call setline(1, ['three', 'four'])")
|
||||
command('write')
|
||||
command('wundo! ' .. undo)
|
||||
|
||||
local blob = assert(t.read_file(undo))
|
||||
local entry = assert(find_entry(blob))
|
||||
|
||||
--- Writes `bytes` over `blob` at `pos` and reads it via :rundo.
|
||||
local function rundo(pos, bytes)
|
||||
t.write_file(undo, blob:sub(1, pos - 1) .. bytes .. blob:sub(pos + #bytes))
|
||||
return pcall_err(command, 'rundo ' .. undo)
|
||||
end
|
||||
|
||||
-- ue_size larger than the file can hold.
|
||||
for _, bad in ipairs({ '\127\255\255\240', '\255\255\255\255' }) do
|
||||
eq(
|
||||
('Vim(rundo):E825: Corrupted undo file (entry size): %s'):format(undo),
|
||||
rundo(entry + 14, bad)
|
||||
)
|
||||
end
|
||||
|
||||
-- Negative ue_top/ue_bot/ue_lcount.
|
||||
for field = 0, 2 do
|
||||
eq(
|
||||
('Vim(rundo):E825: Corrupted undo file (entry lnum): %s'):format(undo),
|
||||
rundo(entry + 2 + field * 4, '\255\255\255\251')
|
||||
)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user