summaryrefslogtreecommitdiffstats
path: root/arch/mips/math-emu/dsemul.c
blob: b6bfd36253694fd24677cad694f9b1e050bd8f89 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
#include <linux/err.h>
#include <linux/slab.h>
#include <linux/mm_types.h>
#include <linux/sched/task.h>

#include <asm/branch.h>
#include <asm/cacheflush.h>
#include <asm/fpu_emulator.h>
#include <asm/inst.h>
#include <asm/mipsregs.h>
#include <linux/uaccess.h>

/**
 * struct emuframe - The 'emulation' frame structure
 * @emul:	The instruction to 'emulate'.
 * @badinst:	A break instruction to cause a return to the kernel.
 *
 * This structure defines the frames placed within the delay slot emulation
 * page in response to a call to mips_dsemul(). Each thread may be allocated
 * only one frame at any given time. The kernel stores within it the
 * instruction to be 'emulated' followed by a break instruction, then
 * executes the frame in user mode. The break causes a trap to the kernel
 * which leads to do_dsemulret() being called unless the instruction in
 * @emul causes a trap itself, is a branch, or a signal is delivered to
 * the thread. In these cases the allocated frame will either be reused by
 * a subsequent delay slot 'emulation', or be freed during signal delivery or
 * upon thread exit.
 *
 * This approach is used because:
 *
 * - Actually emulating all instructions isn't feasible. We would need to
 *   be able to handle instructions from all revisions of the MIPS ISA,
 *   all ASEs & all vendor instruction set extensions. This would be a
 *   whole lot of work & continual maintenance burden as new instructions
 *   are introduced, and in the case of some vendor extensions may not
 *   even be possible. Thus we need to take the approach of actually
 *   executing the instruction.
 *
 * - We must execute the instruction within user context. If we were to
 *   execute the instruction in kernel mode then it would have access to
 *   kernel resources without very careful checks, leaving us with a
 *   high potential for security or stability issues to arise.
 *
 * - We used to place the frame on the users stack, but this requires
 *   that the stack be executable. This is bad for security so the
 *   per-process page is now used instead.
 *
 * - The instruction in @emul may be something entirely invalid for a
 *   delay slot. The user may (intentionally or otherwise) place a branch
 *   in a delay slot, or a kernel mode instruction, or something else
 *   which generates an exception. Thus we can't rely upon the break in
 *   @badinst always being hit. For this reason we track the index of the
 *   frame allocated to each thread, allowing us to clean it up at later
 *   points such as signal delivery or thread exit.
 *
 * - The user may generate a fake struct emuframe if they wish, invoking
 *   the BRK_MEMU break instruction themselves. We must therefore not
 *   trust that BRK_MEMU means there's actually a valid frame allocated
 *   to the thread, and must not allow the user to do anything they
 *   couldn't already.
 */
struct emuframe {
	mips_instruction	emul;
	mips_instruction	badinst;
};

static const int emupage_frame_count = PAGE_SIZE / sizeof(struct emuframe);

static inline __user struct emuframe *dsemul_page(void)
{
	return (__user struct emuframe *)STACK_TOP;
}

static int alloc_emuframe(void)
{
	mm_context_t *mm_ctx = &current->mm->context;
	int idx;

retry:
	spin_lock(&mm_ctx->bd_emupage_lock);

	/* Ensure we have an allocation bitmap */
	if (!mm_ctx->bd_emupage_allocmap) {
		mm_ctx->bd_emupage_allocmap =
			kcalloc(BITS_TO_LONGS(emupage_frame_count),
					      sizeof(unsigned long),
				GFP_ATOMIC);

		if (!mm_ctx->bd_emupage_allocmap) {
			idx = BD_EMUFRAME_NONE;
			goto out_unlock;
		}
	}

	/* Attempt to allocate a single bit/frame */
	idx = bitmap_find_free_region(mm_ctx->bd_emupage_allocmap,
				      emupage_frame_count, 0);
	if (idx < 0) {
		/*
		 * Failed to allocate a frame. We'll wait until one becomes
		 * available. We unlock the page so that other threads actually
		 * get the opportunity to free their frames, which means
		 * technically the result of bitmap_full may be incorrect.
		 * However the worst case is that we repeat all this and end up
		 * back here again.
		 */
		spin_unlock(&mm_ctx->bd_emupage_lock);
		if (!wait_event_killable(mm_ctx->bd_emupage_queue,
			!bitmap_full(mm_ctx->bd_emupage_allocmap,
				     emupage_frame_count)))
			goto retry;

		/* Received a fatal signal - just give in */
		return BD_EMUFRAME_NONE;
	}

	/* Success! */
	pr_debug("allocate emuframe %d to %d\n", idx, current->pid);
out_unlock:
	spin_unlock(&mm_ctx->bd_emupage_lock);
	return idx;
}

static void free_emuframe(int idx, struct mm_struct *mm)
{
	mm_context_t *mm_ctx = &mm->context;

	spin_lock(&mm_ctx->bd_emupage_lock);

	pr_debug("free emuframe %d from %d\n", idx, current->pid);
	bitmap_clear(mm_ctx->bd_emupage_allocmap, idx, 1);

	/* If some thread is waiting for a frame, now's its chance */
	wake_up(&mm_ctx->bd_emupage_queue);

	spin_unlock(&mm_ctx->bd_emupage_lock);
}

static bool within_emuframe(struct pt_regs *regs)
{
	unsigned long base = (unsigned long)dsemul_page();

	if (regs->cp0_epc < base)
		return false;
	if (regs->cp0_epc >= (base + PAGE_SIZE))
		return false;

	return true;
}

bool dsemul_thread_cleanup(struct task_struct *tsk)
{
	int fr_idx;

	/* Clear any allocated frame, retrieving its index */
	fr_idx = atomic_xchg(&tsk->thread.bd_emu_frame, BD_EMUFRAME_NONE);

	/* If no frame was allocated, we're done */
	if (fr_idx == BD_EMUFRAME_NONE)
		return false;

	task_lock(tsk);

	/* Free the frame that this thread had allocated */
	if (tsk->mm)
		free_emuframe(fr_idx, tsk->mm);

	task_unlock(tsk);
	return true;
}

bool dsemul_thread_rollback(struct pt_regs *regs)
{
	struct emuframe __user *fr;
	int fr_idx;

	/* Do nothing if we're not executing from a frame */
	if (!within_emuframe(regs))
		return false;

	/* Find the frame being executed */
	fr_idx = atomic_read(&current->thread.bd_emu_frame);
	if (fr_idx == BD_EMUFRAME_NONE)
		return false;
	fr = &dsemul_page()[fr_idx];

	/*
	 * If the PC is at the emul instruction, roll back to the branch. If
	 * PC is at the badinst (break) instruction, we've already emulated the
	 * instruction so progress to the continue PC. If it's anything else
	 * then something is amiss & the user has branched into some other area
	 * of the emupage - we'll free the allocated frame anyway.
	 */
	if (msk_isa16_mode(regs->cp0_epc) == (unsigned long)&fr->emul)
		regs->cp0_epc = current->thread.bd_emu_branch_pc;
	else if (msk_isa16_mode(regs->cp0_epc) == (unsigned long)&fr->badinst)
		regs->cp0_epc = current->thread.bd_emu_cont_pc;

	atomic_set(&current->thread.bd_emu_frame, BD_EMUFRAME_NONE);
	free_emuframe(fr_idx, current->mm);
	return true;
}

void dsemul_mm_cleanup(struct mm_struct *mm)
{
	mm_context_t *mm_ctx = &mm->context;

	kfree(mm_ctx->bd_emupage_allocmap);
}

int mips_dsemul(struct pt_regs *regs, mips_instruction ir,
		unsigned long branch_pc, unsigned long cont_pc)
{
	int isa16 = get_isa16_mode(regs->cp0_epc);
	mips_instruction break_math;
	struct emuframe __user *fr;
	int err, fr_idx;

	/* NOP is easy */
	if (ir == 0)
		return -1;

	/* microMIPS instructions */
	if (isa16) {
		union mips_instruction insn = { .word = ir };

		/* NOP16 aka MOVE16 $0, $0 */
		if ((ir >> 16) == MM_NOP16)
			return -1;

		/* ADDIUPC */
		if (insn.mm_a_format.opcode == mm_addiupc_op) {
			unsigned int rs;
			s32 v;

			rs = (((insn.mm_a_format.rs + 0xe) & 0xf) + 2);
			v = regs->cp0_epc & ~3;
			v += insn.mm_a_format.simmediate << 2;
			regs->regs[rs] = (long)v;
			return -1;
		}
	}

	pr_debug("dsemul 0x%08lx cont at 0x%08lx\n", regs->cp0_epc, cont_pc);

	/* Allocate a frame if we don't already have one */
	fr_idx = atomic_read(&current->thread.bd_emu_frame);
	if (fr_idx == BD_EMUFRAME_NONE)
		fr_idx = alloc_emuframe();
	if (fr_idx == BD_EMUFRAME_NONE)
		return SIGBUS;
	fr = &dsemul_page()[fr_idx];

	/* Retrieve the appropriately encoded break instruction */
	break_math = BREAK_MATH(isa16);

	/* Write the instructions to the frame */
	if (isa16) {
		err = __put_user(ir >> 16,
				 (u16 __user *)(&fr->emul));
		err |= __put_user(ir & 0xffff,
				  (u16 __user *)((long)(&fr->emul) + 2));
		err |= __put_user(break_math >> 16,
				  (u16 __user *)(&fr->badinst));
		err |= __put_user(break_math & 0xffff,
				  (u16 __user *)((long)(&fr->badinst) + 2));
	} else {
		err = __put_user(ir, &fr->emul);
		err |= __put_user(break_math, &fr->badinst);
	}

	if (unlikely(err)) {
		MIPS_FPU_EMU_INC_STATS(errors);
		free_emuframe(fr_idx, current->mm);
		return SIGBUS;
	}

	/* Record the PC of the branch, PC to continue from & frame index */
	current->thread.bd_emu_branch_pc = branch_pc;
	current->thread.bd_emu_cont_pc = cont_pc;
	atomic_set(&current->thread.bd_emu_frame, fr_idx);

	/* Change user register context to execute the frame */
	regs->cp0_epc = (unsigned long)&fr->emul | isa16;

	/* Ensure the icache observes our newly written frame */
	flush_cache_sigtramp((unsigned long)&fr->emul);

	return 0;
}

bool do_dsemulret(struct pt_regs *xcp)
{
	/* Cleanup the allocated frame, returning if there wasn't one */
	if (!dsemul_thread_cleanup(current)) {
		MIPS_FPU_EMU_INC_STATS(errors);
		return false;
	}

	/* Set EPC to return to post-branch instruction */
	xcp->cp0_epc = current->thread.bd_emu_cont_pc;
	pr_debug("dsemulret to 0x%08lx\n", xcp->cp0_epc);
	MIPS_FPU_EMU_INC_STATS(ds_emul);
	return true;
}