view src/sound.c @ 33581:403d57b06231 v9.0.2035

patch 9.0.2035: [security] use-after-free with wildmenu Commit: https://github.com/vim/vim/commit/8f4fb007e4d472b09ff6bed9ffa485e0c3093699 Author: Yee Cheng Chin <ychin.git@gmail.com> Date: Tue Oct 17 10:06:56 2023 +0200 patch 9.0.2035: [security] use-after-free with wildmenu Problem: [security] use-after-free with wildmenu Solution: properly clean up the wildmenu when exiting Fix wildchar/wildmenu/pum memory corruption with special wildchar's Currently, using `wildchar=<Esc>` or `wildchar=<C-\>` can lead to a memory corruption if using wildmenu+pum, or wrong states if only using wildmenu. This is due to the code only using one single place inside the cmdline process loop to perform wild menu clean up (by checking `end_wildmenu`) but there are other odd situations where the loop could have exited and we need a post-loop clean up just to be sure. If the clean up was not done you would have a stale popup menu referring to invalid memory, or if not using popup menu, incorrect status line (if `laststatus=0`). For example, if you hit `<Esc>` two times when it's wildchar, there's a hard-coded behavior to exit command-line as a failsafe for user, and if you hit `<C-\><C-\><C-N>` it will also exit command-line, but the clean up code would not have hit because of specialized `<C-\>` handling. Fix Ctrl-E / Ctrl-Y to not cancel/accept wildmenu if they are also used for 'wildchar'/'wildcharm'. Currently they don't behave properly, and also have potentially memory unsafe behavior as the logic is currently not accounting for this situation and try to do both. (Previous patch that addressed this: #11677) Also, correctly document Escape key behavior (double-hit it to escape) in wildchar docs as it's previously undocumented. In addition, block known invalid chars to be set in `wildchar` option, such as Ctrl-C and `<CR>`. This is just to make it clear to the user they shouldn't be set, and is not required for this bug fix. closes: #13361 Signed-off-by: Christian Brabandt <cb@256bit.org> Co-authored-by: Yee Cheng Chin <ychin.git@gmail.com>
author Christian Brabandt <cb@256bit.org>
date Tue, 17 Oct 2023 10:15:08 +0200
parents def9fc5c92d1
children 385aaea67d33
line wrap: on
line source

/* vi:set ts=8 sts=4 sw=4 noet:
 *
 * VIM - Vi IMproved	by Bram Moolenaar
 *
 * Do ":help uganda"  in Vim to read copying and usage conditions.
 * Do ":help credits" in Vim to see a list of people who contributed.
 * See README.txt for an overview of the Vim source code.
 */

/*
 * sound.c: functions related making noise
 */

#include "vim.h"

#if defined(FEAT_SOUND) || defined(PROTO)

static long	    sound_id = 0;

// soundcb_T is typedef'ed in proto/sound.pro

struct soundcb_S {
    callback_T	snd_callback;
#ifdef MSWIN
    MCIDEVICEID	snd_device_id;
    long	snd_id;
#endif
    soundcb_T	*snd_next;
};

static soundcb_T    *first_callback = NULL;

/*
 * Return TRUE when a sound callback has been created, it may be invoked when
 * the sound finishes playing.  Also see has_sound_callback_in_queue().
 */
    int
has_any_sound_callback(void)
{
    return first_callback != NULL;
}

    static soundcb_T *
get_sound_callback(typval_T *arg)
{
    callback_T	callback;
    soundcb_T	*soundcb;

    if (arg->v_type == VAR_UNKNOWN)
	return NULL;
    callback = get_callback(arg);
    if (callback.cb_name == NULL)
	return NULL;

    soundcb = ALLOC_ONE(soundcb_T);
    if (soundcb == NULL)
    {
	free_callback(&callback);
	return NULL;
    }

    soundcb->snd_next = first_callback;
    first_callback = soundcb;
    set_callback(&soundcb->snd_callback, &callback);
    if (callback.cb_free_name)
	vim_free(callback.cb_name);
    return soundcb;
}

/*
 * Call "soundcb" with proper parameters.
 */
    void
call_sound_callback(soundcb_T *soundcb, long snd_id, int result)
{
    typval_T	argv[3];
    typval_T	rettv;

    argv[0].v_type = VAR_NUMBER;
    argv[0].vval.v_number = snd_id;
    argv[1].v_type = VAR_NUMBER;
    argv[1].vval.v_number = result;
    argv[2].v_type = VAR_UNKNOWN;

    call_callback(&soundcb->snd_callback, -1, &rettv, 2, argv);
    clear_tv(&rettv);
}

/*
 * Delete "soundcb" from the list of pending callbacks.
 */
    void
delete_sound_callback(soundcb_T *soundcb)
{
    soundcb_T	*p;
    soundcb_T	*prev = NULL;

    for (p = first_callback; p != NULL; prev = p, p = p->snd_next)
	if (p == soundcb)
	{
	    if (prev == NULL)
		first_callback = p->snd_next;
	    else
		prev->snd_next = p->snd_next;
	    free_callback(&p->snd_callback);
	    vim_free(p);
	    break;
	}
}

#if defined(HAVE_CANBERRA) || defined(PROTO)

/*
 * Sound implementation for Linux/Unix using libcanberra.
 */
# include <canberra.h>

static ca_context   *context = NULL;

// Structure to store info about a sound callback to be invoked soon.
typedef struct soundcb_queue_S soundcb_queue_T;

struct soundcb_queue_S {
    soundcb_queue_T	*scb_next;
    uint32_t		scb_id;		// ID of the sound
    int			scb_result;	// CA_ value
    soundcb_T		*scb_callback;	// function to call
};

// Queue of callbacks to invoke from the main loop.
static soundcb_queue_T *callback_queue = NULL;

/*
 * Add a callback to the queue of callbacks to invoke later from the main loop.
 * That is because the callback may be called from another thread and invoking
 * another sound function may cause trouble.
 */
    static void
sound_callback(
	ca_context  *c UNUSED,
	uint32_t    id,
	int	    error_code,
	void	    *userdata)
{
    soundcb_T	    *soundcb = (soundcb_T *)userdata;
    soundcb_queue_T *scb;

    scb = ALLOC_ONE(soundcb_queue_T);
    if (scb == NULL)
	return;
    scb->scb_next = callback_queue;
    callback_queue = scb;
    scb->scb_id = id;
    scb->scb_result = error_code == CA_SUCCESS ? 0
			  : error_code == CA_ERROR_CANCELED
					    || error_code == CA_ERROR_DESTROYED
			  ? 1 : 2;
    scb->scb_callback = soundcb;
}

/*
 * Return TRUE if there is a sound callback to be called.
 */
    int
has_sound_callback_in_queue(void)
{
    return callback_queue != NULL;
}

/*
 * Invoke queued sound callbacks.
 */
    void
invoke_sound_callback(void)
{
    soundcb_queue_T *scb;

    while (callback_queue != NULL)
    {
	scb = callback_queue;
	callback_queue = scb->scb_next;

	call_sound_callback(scb->scb_callback, scb->scb_id, scb->scb_result);

	delete_sound_callback(scb->scb_callback);
	vim_free(scb);
    }
    redraw_after_callback(TRUE, FALSE);
}

    static void
sound_play_common(typval_T *argvars, typval_T *rettv, int playfile)
{
    if (in_vim9script() && check_for_string_arg(argvars, 0) == FAIL)
	return;

    if (context == NULL)
	ca_context_create(&context);
    if (context == NULL)
	return;

    soundcb_T	*soundcb = get_sound_callback(&argvars[1]);
    int		res = CA_ERROR_INVALID;

    ++sound_id;
    if (soundcb == NULL)
    {
	res = ca_context_play(context, sound_id,
		playfile ? CA_PROP_MEDIA_FILENAME : CA_PROP_EVENT_ID,
		tv_get_string(&argvars[0]),
		CA_PROP_CANBERRA_CACHE_CONTROL, "volatile",
		NULL);
    }
    else
    {
	static ca_proplist *proplist = NULL;

	ca_proplist_create(&proplist);
	if (proplist != NULL)
	{
	    if (playfile)
		ca_proplist_sets(proplist, CA_PROP_MEDIA_FILENAME,
			(char *)tv_get_string(&argvars[0]));
	    else
		ca_proplist_sets(proplist, CA_PROP_EVENT_ID,
			(char *)tv_get_string(&argvars[0]));
	    ca_proplist_sets(proplist, CA_PROP_CANBERRA_CACHE_CONTROL,
		    "volatile");
	    res = ca_context_play_full(context, sound_id, proplist,
		    sound_callback, soundcb);
	    if (res != CA_SUCCESS)
		delete_sound_callback(soundcb);

	    ca_proplist_destroy(proplist);
	}
    }
    rettv->vval.v_number = res == CA_SUCCESS ? sound_id : 0;
}

    void
f_sound_playevent(typval_T *argvars, typval_T *rettv)
{
    sound_play_common(argvars, rettv, FALSE);
}

/*
 * implementation of sound_playfile({path} [, {callback}])
 */
    void
f_sound_playfile(typval_T *argvars, typval_T *rettv)
{
    sound_play_common(argvars, rettv, TRUE);
}

/*
 * implementation of sound_stop({id})
 */
    void
f_sound_stop(typval_T *argvars, typval_T *rettv UNUSED)
{
    if (in_vim9script() && check_for_number_arg(argvars, 0) == FAIL)
	return;

    if (context != NULL)
	ca_context_cancel(context, tv_get_number(&argvars[0]));
}

/*
 * implementation of sound_clear()
 */
    void
f_sound_clear(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
{
    if (context == NULL)
	return;
    ca_context_destroy(context);
    context = NULL;
}

# if defined(EXITFREE) || defined(PROTO)
    void
sound_free(void)
{
    soundcb_queue_T *scb;

    if (context != NULL)
	ca_context_destroy(context);

    while (first_callback != NULL)
	delete_sound_callback(first_callback);

    while (callback_queue != NULL)
    {
	scb = callback_queue;
	callback_queue = scb->scb_next;
	delete_sound_callback(scb->scb_callback);
	vim_free(scb);
    }
}
# endif

#elif defined(MSWIN)

/*
 * Sound implementation for MS-Windows.
 */

static HWND g_hWndSound = NULL;

    static LRESULT CALLBACK
sound_wndproc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    soundcb_T	*p;

    switch (message)
    {
	case MM_MCINOTIFY:
	    for (p = first_callback; p != NULL; p = p->snd_next)
		if (p->snd_device_id == (MCIDEVICEID) lParam)
		{
		    char	buf[32];

		    vim_snprintf(buf, sizeof(buf), "close sound%06ld",
								p->snd_id);
		    mciSendString(buf, NULL, 0, 0);

		    long result =   wParam == MCI_NOTIFY_SUCCESSFUL ? 0
				  : wParam == MCI_NOTIFY_ABORTED ? 1 : 2;
		    call_sound_callback(p, p->snd_id, result);

		    delete_sound_callback(p);
		    redraw_after_callback(TRUE, FALSE);

		}
	    break;
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

    static HWND
sound_window(void)
{
    if (g_hWndSound == NULL)
    {
	LPCSTR clazz = "VimSound";
	WNDCLASS wndclass = {
	    0, sound_wndproc, 0, 0, g_hinst, NULL, 0, 0, NULL, clazz };
	RegisterClass(&wndclass);
	g_hWndSound = CreateWindow(clazz, NULL, 0, 0, 0, 0, 0,
		HWND_MESSAGE, NULL, g_hinst, NULL);
    }

    return g_hWndSound;
}

    void
f_sound_playevent(typval_T *argvars, typval_T *rettv)
{
    WCHAR	    *wp;

    if (in_vim9script() && check_for_string_arg(argvars, 0) == FAIL)
	return;

    wp = enc_to_utf16(tv_get_string(&argvars[0]), NULL);
    if (wp == NULL)
	return;

    if (PlaySoundW(wp, NULL, SND_ASYNC | SND_ALIAS))
	rettv->vval.v_number = ++sound_id;
    free(wp);
}

    void
f_sound_playfile(typval_T *argvars, typval_T *rettv)
{
    long	newid = sound_id + 1;
    size_t	len;
    char_u	*p, *esc;
    WCHAR	*wp;
    soundcb_T	*soundcb;
    char	buf[32];
    MCIERROR	err;

    if (in_vim9script() && check_for_string_arg(argvars, 0) == FAIL)
	return;

    esc = vim_strsave_shellescape(tv_get_string(&argvars[0]), FALSE, FALSE);

    len = STRLEN(esc) + 5 + 18 + 1;
    p = alloc(len);
    if (p == NULL)
    {
	free(esc);
	return;
    }
    vim_snprintf((char *)p, len, "open %s alias sound%06ld", esc, newid);
    free(esc);

    wp = enc_to_utf16((char_u *)p, NULL);
    free(p);
    if (wp == NULL)
	return;

    err = mciSendStringW(wp, NULL, 0, sound_window());
    free(wp);
    if (err != 0)
	return;

    vim_snprintf(buf, sizeof(buf), "play sound%06ld notify", newid);
    err = mciSendString(buf, NULL, 0, sound_window());
    if (err != 0)
	goto failure;

    sound_id = newid;
    rettv->vval.v_number = sound_id;

    soundcb = get_sound_callback(&argvars[1]);
    if (soundcb != NULL)
    {
	vim_snprintf(buf, sizeof(buf), "sound%06ld", newid);
	soundcb->snd_id = newid;
	soundcb->snd_device_id = mciGetDeviceID(buf);
    }
    return;

failure:
    vim_snprintf(buf, sizeof(buf), "close sound%06ld", newid);
    mciSendString(buf, NULL, 0, NULL);
}

    void
f_sound_stop(typval_T *argvars, typval_T *rettv UNUSED)
{
    long    id;
    char    buf[32];

    if (in_vim9script() && check_for_number_arg(argvars, 0) == FAIL)
	return;

    id = tv_get_number(&argvars[0]);
    vim_snprintf(buf, sizeof(buf), "stop sound%06ld", id);
    mciSendString(buf, NULL, 0, NULL);
}

    void
f_sound_clear(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
{
    PlaySoundW(NULL, NULL, 0);
    mciSendString("close all", NULL, 0, NULL);
}

# if defined(EXITFREE)
    void
sound_free(void)
{
    CloseWindow(g_hWndSound);

    while (first_callback != NULL)
	delete_sound_callback(first_callback);
}
# endif

#elif defined(MACOS_X_DARWIN)

// Sound implementation for macOS.
    static void
sound_play_common(typval_T *argvars, typval_T *rettv, bool playfile)
{
    if (in_vim9script() && check_for_string_arg(argvars, 0) == FAIL)
	return;

    char_u *sound_name = tv_get_string(&argvars[0]);
    soundcb_T *soundcb = get_sound_callback(&argvars[1]);

    ++sound_id;

    bool play_success = sound_mch_play(sound_name, sound_id, soundcb, playfile);
    if (!play_success && soundcb)
    {
	delete_sound_callback(soundcb);
    }
    rettv->vval.v_number = play_success ? sound_id : 0;
}

    void
f_sound_playevent(typval_T *argvars, typval_T *rettv)
{
    sound_play_common(argvars, rettv, false);
}

    void
f_sound_playfile(typval_T *argvars, typval_T *rettv)
{
    sound_play_common(argvars, rettv, true);
}

    void
f_sound_stop(typval_T *argvars, typval_T *rettv UNUSED)
{
    if (in_vim9script() && check_for_number_arg(argvars, 0) == FAIL)
	return;
    sound_mch_stop(tv_get_number(&argvars[0]));
}

    void
f_sound_clear(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
{
    sound_mch_clear();
}

#if defined(EXITFREE) || defined(PROTO)
    void
sound_free(void)
{
    sound_mch_free();
    while (first_callback != NULL)
	delete_sound_callback(first_callback);
}
#endif

#endif // MACOS_X_DARWIN

#endif  // FEAT_SOUND