Commit message: Revert "consolemap: Fix a memory leaking bug in drivers/tty/vt/consolemap.c"

This reverts commit 84ecc2f6eb1cb12e6d44818f94fa49b50f06e6ac.

con_insert_unipair() is working with a sparse 3-dimensional array:

- p->uni_pgdir[] is the top layer
- p1 points to a middle layer
- p2 points to a bottom layer

If it needs to allocate a new middle layer, and then fails to allocate
a new bottom layer, it would previously free only p2, and now it frees
both p1 and p2.  But since the new middle layer was already registered
in the top layer, it was not leaked.

However, if it looks up an *existing* middle layer and then fails to
allocate a bottom layer, it now frees both p1 and p2 but does *not*
free any other bottom layers under p1.  So it *introduces* a memory
leak.

The error path also cleared the wrong index in p->uni_pgdir[],
introducing a use-after-free.

Signed-off-by: Ben Hutchings <ben@decadent.org.uk>
Fixes: 84ecc2f6eb1c ("consolemap: Fix a memory leaking bug in drivers/tty/vt/consolemap.c")
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
CVE: CVE-2019-12379
CWE: CWE-401
Vulnerability description: An issue was discovered in con_insert_unipair in drivers/tty/vt/consolemap.c in the Linux kernel through 5.1.5. There is a memory leak in a certain case of an ENOMEM outcome of kmalloc. NOTE: This id is disputed as not being an issue

Project: tty
File: consolemap.c

int con_set_default_unimap(struct vc_data *vc)
{
	int i, j, err = 0, err1;
	u16 *q;
	struct uni_pagedir *p;

	if (dflt) {
		p = *vc->vc_uni_pagedir_loc;
		if (p == dflt)
			return 0;

		dflt->refcount++;
		*vc->vc_uni_pagedir_loc = dflt;
		if (p && !--p->refcount) {
			con_release_unimap(p);
			kfree(p);
		}
		return 0;
	}
	
	/* The default font is always 256 characters */

	err = con_do_clear_unimap(vc);
	if (err)
		return err;
    
	p = *vc->vc_uni_pagedir_loc;
	q = dfont_unitable;
	
	for (i = 0; i < 256; i++)
		for (j = dfont_unicount[i]; j; j--) {
			err1 = con_insert_unipair(p, *(q++), i);
			if (err1)
				err = err1;
		}
			
	if (con_unify_unimap(vc, p)) {
		dflt = *vc->vc_uni_pagedir_loc;
		return err;
	}

	for (i = 0; i <= 3; i++)
		set_inverse_transl(vc, p, i);	/* Update all inverse translations */
	set_inverse_trans_unicode(vc, p);
	dflt = p;
	return err;
}

Project: tty
File: consolemap.c

int con_get_trans_new(ushort __user * arg)
{
	unsigned short outbuf[E_TABSZ];

	console_lock();
	memcpy(outbuf, translations[USER_MAP], sizeof(outbuf));
	console_unlock();

	return copy_to_user(arg, outbuf, sizeof(outbuf)) ? -EFAULT : 0;
}

Project: tty
File: consolemap.c

int con_get_trans_old(unsigned char __user * arg)
{
	int i, ch;
	unsigned short *p = translations[USER_MAP];
	unsigned char outbuf[E_TABSZ];

	console_lock();
	for (i = 0; i < E_TABSZ ; i++)
	{
		ch = conv_uni_to_pc(vc_cons[fg_console].d, p[i]);
		outbuf[i] = (ch & ~0xff) ? 0 : ch;
	}
	console_unlock();

	return copy_to_user(arg, outbuf, sizeof(outbuf)) ? -EFAULT : 0;
}

Project: tty
File: consolemap.c

int con_set_trans_new(ushort __user * arg)
{
	unsigned short inbuf[E_TABSZ];

	if (copy_from_user(inbuf, arg, sizeof(inbuf)))
		return -EFAULT;

	console_lock();
	memcpy(translations[USER_MAP], inbuf, sizeof(inbuf));
	update_user_maps();
	console_unlock();
	return 0;
}

Project: tty
File: consolemap.c

u16 inverse_translate(struct vc_data *conp, int glyph, int use_unicode)
{
	struct uni_pagedir *p;
	int m;
	if (glyph < 0 || glyph >= MAX_GLYPH)
		return 0;
	else {
		p = *conp->vc_uni_pagedir_loc;
		if (!p)
			return glyph;
		else if (use_unicode) {
			if (!p->inverse_trans_unicode)
				return glyph;
			else
				return p->inverse_trans_unicode[glyph];
			} else {
			m = inv_translate[conp->vc_num];
			if (!p->inverse_translations[m])
				return glyph;
			else
				return p->inverse_translations[m][glyph];
			}
	}
}

Project: tty
File: consolemap.c

void con_free_unimap(struct vc_data *vc)
{
	struct uni_pagedir *p;

	p = *vc->vc_uni_pagedir_loc;
	if (!p)
		return;
	*vc->vc_uni_pagedir_loc = NULL;
	if (--p->refcount)
		return;
	con_release_unimap(p);
	kfree(p);
}

Project: tty
File: consolemap.c

int con_set_trans_old(unsigned char __user * arg)
{
	int i;
	unsigned short inbuf[E_TABSZ];
	unsigned char ubuf[E_TABSZ];

	if (copy_from_user(ubuf, arg, E_TABSZ))
		return -EFAULT;

	for (i = 0; i < E_TABSZ ; i++)
		inbuf[i] = UNI_DIRECT_BASE | ubuf[i];

	console_lock();
	memcpy(translations[USER_MAP], inbuf, sizeof(inbuf));
	update_user_maps();
	console_unlock();
	return 0;
}

Project: tty
File: consolemap.c

static void set_inverse_trans_unicode(struct vc_data *conp,
				      struct uni_pagedir *p)
{
	int i, j, k, glyph;
	u16 **p1, *p2;
	u16 *q;

	if (!p) return;
	q = p->inverse_trans_unicode;
	if (!q) {
		q = p->inverse_trans_unicode =
			kmalloc_array(MAX_GLYPH, sizeof(u16), GFP_KERNEL);
		if (!q)
			return;
	}
	memset(q, 0, MAX_GLYPH * sizeof(u16));

	for (i = 0; i < 32; i++) {
		p1 = p->uni_pgdir[i];
		if (!p1)
			continue;
		for (j = 0; j < 32; j++) {
			p2 = p1[j];
			if (!p2)
				continue;
			for (k = 0; k < 64; k++) {
				glyph = p2[k];
				if (glyph >= 0 && glyph < MAX_GLYPH
					       && q[glyph] < 32)
		  			q[glyph] = (i << 11) + (j << 6) + k;
			}
		}
	}
}

Project: tty
File: consolemap.c

console_map_init(void)
{
	int i;
	
	for (i = 0; i < MAX_NR_CONSOLES; i++)
		if (vc_cons_allocated(i) && !*vc_cons[i].d->vc_uni_pagedir_loc)
			con_set_default_unimap(vc_cons[i].d);
}

Project: tty
File: consolemap.c

int con_copy_unimap(struct vc_data *dst_vc, struct vc_data *src_vc)
{
	struct uni_pagedir *q;

	if (!*src_vc->vc_uni_pagedir_loc)
		return -EINVAL;
	if (*dst_vc->vc_uni_pagedir_loc == *src_vc->vc_uni_pagedir_loc)
		return 0;
	con_free_unimap(dst_vc);
	q = *src_vc->vc_uni_pagedir_loc;
	q->refcount++;
	*dst_vc->vc_uni_pagedir_loc = q;
	return 0;
}

Project: tty
File: consolemap.c

static void update_user_maps(void)
{
	int i;
	struct uni_pagedir *p, *q = NULL;
	
	for (i = 0; i < MAX_NR_CONSOLES; i++) {
		if (!vc_cons_allocated(i))
			continue;
		p = *vc_cons[i].d->vc_uni_pagedir_loc;
		if (p && p != q) {
			set_inverse_transl(vc_cons[i].d, p, USER_MAP);
			set_inverse_trans_unicode(vc_cons[i].d, p);
			q = p;
		}
	}
}

Project: tty
File: consolemap.c

static void set_inverse_transl(struct vc_data *conp, struct uni_pagedir *p, int i)
{
	int j, glyph;
	unsigned short *t = translations[i];
	unsigned char *q;
	
	if (!p) return;
	q = p->inverse_translations[i];

	if (!q) {
		q = p->inverse_translations[i] = kmalloc(MAX_GLYPH, GFP_KERNEL);
		if (!q) return;
	}
	memset(q, 0, MAX_GLYPH);

	for (j = 0; j < E_TABSZ; j++) {
		glyph = conv_uni_to_pc(conp, t[j]);
		if (glyph >= 0 && glyph < MAX_GLYPH && q[glyph] < 32) {
			/* prefer '-' above SHY etc. */
		  	q[glyph] = j;
		}
	}
}

Project: tty
File: consolemap.c

int conv_uni_to_8bit(u32 uni)
{
	int c;
	for (c = 0; c < 0x100; c++)
		if (translations[USER_MAP][c] == uni ||
		   (translations[USER_MAP][c] == (c | 0xf000) && uni == c))
			return c;
	return -1;
}

Project: tty
File: consolemap.c

static void con_release_unimap(struct uni_pagedir *p)
{
	u16 **p1;
	int i, j;

	if (p == dflt) dflt = NULL;  
	for (i = 0; i < 32; i++) {
		p1 = p->uni_pgdir[i];
		if (p1 != NULL) {
			for (j = 0; j < 32; j++)
				kfree(p1[j]);
			kfree(p1);
		}
		p->uni_pgdir[i] = NULL;
	}
	for (i = 0; i < 4; i++) {
		kfree(p->inverse_translations[i]);
		p->inverse_translations[i] = NULL;
	}
	kfree(p->inverse_trans_unicode);
	p->inverse_trans_unicode = NULL;
}

Project: tty
File: consolemap.c

int con_set_unimap(struct vc_data *vc, ushort ct, struct unipair __user *list)
{
	int err = 0, err1, i;
	struct uni_pagedir *p, *q;
	struct unipair *unilist, *plist;

	if (!ct)
		return 0;

	unilist = vmemdup_user(list, ct * sizeof(struct unipair));
	if (IS_ERR(unilist))
		return PTR_ERR(unilist);

	console_lock();

	/* Save original vc_unipagdir_loc in case we allocate a new one */
	p = *vc->vc_uni_pagedir_loc;

	if (!p) {
		err = -EINVAL;

		goto out_unlock;
	}
	
	if (p->refcount > 1) {
		int j, k;
		u16 **p1, *p2, l;
		
		err1 = con_do_clear_unimap(vc);
		if (err1) {
			err = err1;
			goto out_unlock;
		}
		
		/*
		 * Since refcount was > 1, con_clear_unimap() allocated a
		 * a new uni_pagedir for this vc.  Re: p != q
		 */
		q = *vc->vc_uni_pagedir_loc;

		/*
		 * uni_pgdir is a 32*32*64 table with rows allocated
		 * when its first entry is added.  The unicode value must
		 * still be incremented for empty rows.  We are copying
		 * entries from "p" (old) to "q" (new).
		 */
		l = 0;		/* unicode value */
		for (i = 0; i < 32; i++) {
		p1 = p->uni_pgdir[i];
		if (p1)
			for (j = 0; j < 32; j++) {
			p2 = p1[j];
			if (p2) {
				for (k = 0; k < 64; k++, l++)
				if (p2[k] != 0xffff) {
					/*
					 * Found one, copy entry for unicode
					 * l with fontpos value p2[k].
					 */
					err1 = con_insert_unipair(q, l, p2[k]);
					if (err1) {
						p->refcount++;
						*vc->vc_uni_pagedir_loc = p;
						con_release_unimap(q);
						kfree(q);
						err = err1;
						goto out_unlock;
					}
				}
			} else {
				/* Account for row of 64 empty entries */
				l += 64;
			}
		}
		else
			/* Account for empty table */
			l += 32 * 64;
		}

		/*
		 * Finished copying font table, set vc_uni_pagedir to new table
		 */
		p = q;
	} else if (p == dflt) {
		dflt = NULL;
	}

	/*
	 * Insert user specified unicode pairs into new table.
	 */
	for (plist = unilist; ct; ct--, plist++) {
		err1 = con_insert_unipair(p, plist->unicode, plist->fontpos);
		if (err1)
			err = err1;
	}
	
	/*
	 * Merge with fontmaps of any other virtual consoles.
	 */
	if (con_unify_unimap(vc, p))
		goto out_unlock;

	for (i = 0; i <= 3; i++)
		set_inverse_transl(vc, p, i); /* Update inverse translations */
	set_inverse_trans_unicode(vc, p);

out_unlock:
	console_unlock();
	kvfree(unilist);
	return err;
}

Project: tty
File: consolemap.c

unsigned short *set_translate(int m, struct vc_data *vc)
{
	inv_translate[vc->vc_num] = m;
	return translations[m];
}

Project: tty
File: consolemap.c

conv_uni_to_pc(struct vc_data *conp, long ucs) 
{
	int h;
	u16 **p1, *p2;
	struct uni_pagedir *p;
  
	/* Only 16-bit codes supported at this time */
	if (ucs > 0xffff)
		return -4;		/* Not found */
	else if (ucs < 0x20)
		return -1;		/* Not a printable character */
	else if (ucs == 0xfeff || (ucs >= 0x200b && ucs <= 0x200f))
		return -2;			/* Zero-width space */
	/*
	 * UNI_DIRECT_BASE indicates the start of the region in the User Zone
	 * which always has a 1:1 mapping to the currently loaded font.  The
	 * UNI_DIRECT_MASK indicates the bit span of the region.
	 */
	else if ((ucs & ~UNI_DIRECT_MASK) == UNI_DIRECT_BASE)
		return ucs & UNI_DIRECT_MASK;
  
	if (!*conp->vc_uni_pagedir_loc)
		return -3;

	p = *conp->vc_uni_pagedir_loc;
	if ((p1 = p->uni_pgdir[ucs >> 11]) &&
	    (p2 = p1[(ucs >> 6) & 0x1f]) &&
	    (h = p2[ucs & 0x3f]) < MAX_GLYPH)
		return h;

	return -4;		/* not found */
}

Project: tty
File: consolemap.c

int con_get_unimap(struct vc_data *vc, ushort ct, ushort __user *uct, struct unipair __user *list)
{
	int i, j, k, ret = 0;
	ushort ect;
	u16 **p1, *p2;
	struct uni_pagedir *p;
	struct unipair *unilist;

	unilist = kvmalloc_array(ct, sizeof(struct unipair), GFP_KERNEL);
	if (!unilist)
		return -ENOMEM;

	console_lock();

	ect = 0;
	if (*vc->vc_uni_pagedir_loc) {
		p = *vc->vc_uni_pagedir_loc;
		for (i = 0; i < 32; i++) {
		p1 = p->uni_pgdir[i];
		if (p1)
			for (j = 0; j < 32; j++) {
			p2 = *(p1++);
			if (p2)
				for (k = 0; k < 64; k++, p2++) {
					if (*p2 >= MAX_GLYPH)
						continue;
					if (ect < ct) {
						unilist[ect].unicode =
							(i<<11)+(j<<6)+k;
						unilist[ect].fontpos = *p2;
					}
					ect++;
				}
			}
		}
	}
	console_unlock();
	if (copy_to_user(list, unilist, min(ect, ct) * sizeof(struct unipair)))
		ret = -EFAULT;
	put_user(ect, uct);
	kvfree(unilist);
	return ret ? ret : (ect <= ct) ? 0 : -ENOMEM;
}

Project: tty
File: consolemap.c

int con_clear_unimap(struct vc_data *vc)
{
	int ret;
	console_lock();
	ret = con_do_clear_unimap(vc);
	console_unlock();
	return ret;
}

Project: tty
File: consolemap.c

static int con_do_clear_unimap(struct vc_data *vc)
{
	struct uni_pagedir *p, *q;

	p = *vc->vc_uni_pagedir_loc;
	if (!p || --p->refcount) {
		q = kzalloc(sizeof(*p), GFP_KERNEL);
		if (!q) {
			if (p)
				p->refcount++;
			return -ENOMEM;
		}
		q->refcount=1;
		*vc->vc_uni_pagedir_loc = q;
	} else {
		if (p == dflt) dflt = NULL;
		p->refcount++;
		p->sum = 0;
		con_release_unimap(p);
	}
	return 0;
}

Project: tty
File: consolemap.c

u32 conv_8bit_to_uni(unsigned char c)
{
	unsigned short uni = translations[USER_MAP][c];
	return uni == (0xf000 | c) ? c : uni;
}

Project: tty
File: consolemap.c

static int con_unify_unimap(struct vc_data *conp, struct uni_pagedir *p)
{
	int i, j, k;
	struct uni_pagedir *q;
	
	for (i = 0; i < MAX_NR_CONSOLES; i++) {
		if (!vc_cons_allocated(i))
			continue;
		q = *vc_cons[i].d->vc_uni_pagedir_loc;
		if (!q || q == p || q->sum != p->sum)
			continue;
		for (j = 0; j < 32; j++) {
			u16 **p1, **q1;
			p1 = p->uni_pgdir[j]; q1 = q->uni_pgdir[j];
			if (!p1 && !q1)
				continue;
			if (!p1 || !q1)
				break;
			for (k = 0; k < 32; k++) {
				if (!p1[k] && !q1[k])
					continue;
				if (!p1[k] || !q1[k])
					break;
				if (memcmp(p1[k], q1[k], 64*sizeof(u16)))
					break;
			}
			if (k < 32)
				break;
		}
		if (j == 32) {
			q->refcount++;
			*conp->vc_uni_pagedir_loc = q;
			con_release_unimap(p);
			kfree(p);
			return 1;
		}
	}
	return 0;
}