소싯적에 뜻한 바가 있어서 인텔 가상화 기술(Intel Virtualization Technology, VT-x/VT-d)를 직접 이용하여 리눅스 커널을 보호하는 소프트웨어를 만들었습니다. ^^;;; 이름은 Shadow-box라고 지었는데요, 설계와 구현물은 오픈소스로 공개되어 있고 github.com/kkamagui/shadow-box-for-x86에서 보실 수 있습니다. 최근에 커널의 메모리 관련 디버깅 옵션과 연계하는데 문제가 있어서 업그레이드를 힘겹게 진행했는데요, 시험 중에 재미있는 현상을 발견해서 한자 남겨봅니다.

Shadow-box는 가상화 기술을 사용해서 실제 보호 기능을 수행하는 호스트(Host) 영역과 호스트에 의해 보호받는 일반 영역인 게스트(Guest) 영역으로 구분합니다. 여기서 재미있는 부분은 호스트와 게스트가 커널을 공유한다는 점인데요, 공유함과 동시에 호스트에서 게스트 커널의 주요 영역이 변조되었는지를 이벤트 기반(Event-driven) 혹은 주기적 감시(Periodic monitoring)를 통해 확인한다는 겁니다.

Shadow-box의 구조

 비록 호스트는 게스트의 리눅스 커널을 공유하지만 모든 기능을 사용할 수 는 없습니다. 호스트는 적어도 변조가 되지 않은 리눅스 커널 영역에서 구동되어야 하고 혹여 태스크 스위칭이 되어 호스트에서 게스트 프로세스가 실행되거나 하면 안 되기 때문이죠. 그래서 호스트에서는 철저하게 이를 막고 있는데요,  호스트에서 인터럽트 비활성화(Interrupt Disable)와 선점 불가(Preemption Disable)를 사용합니다. ^^;;;

인터럽트 비활성화의 경우는 VT-x가 직접 처리해주는 부분이라 Shadow-box에서 별도의 처리는 하지 않고, 선점 불가의 경우는 리눅스 커널의 preemption_disable(), preemption_enable() 함수를 사용해서 처리하고 있습니다. 그런데 예상치도 못한 변수를 만났습니다. 바로 뮤텍스(Mutex)와 스핀락(Spinlock) 때문이죠.

뮤텍스(Mutex)와 스핀락(Spinlock)은 커널에서 흔히 그리고 전통적으로 사용되었던 동기화 기재(Synchronization)인데요, 뮤텍스는 대기(Sleep)를 전제로 하고 있고 스핀락은 바쁜 대기(Busy-waiting)를 전제로 하고 있습니다. 좀 더 자세히 설명하자면, 뮤텍스의 경우는 이미 누군가가 뮤텍스를 사용(Lock)하고 있다면 다른 사람은 해당 뮤텍스의 대기 큐에 자신을 등록하고 차례가 올 때까지 대기(Sleep) 합니다. 그리고 앞서 뮤텍스를 잡은 사람이 이를 해제(Unlock)하면 대기 큐에 있는 프로세스를 깨우는 것이죠. 그리고 즉시 해당 프로세스가 실행되는 특징이 있습니다. 스핀락의 경우는 누군가가 잡고(Lock) 사용 중이면 다른 사람은 계속 루프를 돌면서 해제(Unlock) 되었는지를 확인합니다. 그리고 앞서 스핀락을 잡은 사람이 이른 해제(Unlock)하면 루프를 돌면서 확인하던 다른 사람은 이를 감지하고 다시 잡은 후 작업을 시작하게 되는 것이죠. 이미 눈치를 채셨겠지만, 대기 큐가 없고 순서도 없는 야생인 것이죠. ^^;;;

실제 리눅스 커널의 뮤텍스의 Unlock 코드는 아래처럼 되어 있습니다. 대기 큐(Wait Queue)를 돌면서 태스크를 깨우는 것을 알 수 있습니다.

static noinline void __sched __mutex_unlock_slowpath(struct mutex *lock, unsigned long ip)
{
	struct task_struct *next = NULL;
	DEFINE_WAKE_Q(wake_q);
	unsigned long owner;

	mutex_release(&lock->dep_map, ip);

	/*
	 * Release the lock before (potentially) taking the spinlock such that
	 * other contenders can get on with things ASAP.
	 *
	 * Except when HANDOFF, in that case we must not clear the owner field,
	 * but instead set it to the top waiter.
	 */
	owner = atomic_long_read(&lock->owner);
	for (;;) {
		unsigned long old;

#ifdef CONFIG_DEBUG_MUTEXES
		DEBUG_LOCKS_WARN_ON(__owner_task(owner) != current);
		DEBUG_LOCKS_WARN_ON(owner & MUTEX_FLAG_PICKUP);
#endif

		if (owner & MUTEX_FLAG_HANDOFF)
			break;

		old = atomic_long_cmpxchg_release(&lock->owner, owner,
						  __owner_flags(owner));
		if (old == owner) {
			if (owner & MUTEX_FLAG_WAITERS)
				break;

			return;
		}

		owner = old;
	}

	spin_lock(&lock->wait_lock);
	debug_mutex_unlock(lock);
	if (!list_empty(&lock->wait_list)) {
		/* get the first entry from the wait-list: */
		struct mutex_waiter *waiter =
			list_first_entry(&lock->wait_list,
					 struct mutex_waiter, list);

		next = waiter->task;

		debug_mutex_wake_waiter(lock, waiter);
		wake_q_add(&wake_q, next);
	}

	if (owner & MUTEX_FLAG_HANDOFF)
		__mutex_handoff(lock, next);

	spin_unlock(&lock->wait_lock);

	wake_up_q(&wake_q);    
}


void wake_up_q(struct wake_q_head *head)
{
	struct wake_q_node *node = head->first;

	while (node != WAKE_Q_TAIL) {
		struct task_struct *task;

		task = container_of(node, struct task_struct, wake_q);
		BUG_ON(!task);
		/* Task can safely be re-inserted now: */
		node = node->next;
		task->wake_q.next = NULL;

		/*
		 * wake_up_process() executes a full barrier, which pairs with
		 * the queueing in wake_q_add() so as not to miss wakeups.
		 */
		wake_up_process(task);
		put_task_struct(task);
	}
}

스핀락의 Unlock은 아래와 같은데요, 이 역시 Unlock과 함께 스케줄러를 호출하는 것을 알 수 있습니다. 단, preemptible 일 때만 그렇게 하는데, 인터럽트가 활성화되어 있고 preempt_enable()인 상태여야 합니다.

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, _RET_IP_);
	do_raw_spin_unlock(lock);
	preempt_enable();
}

#define preempt_enable() \
do { \
	barrier(); \
	if (unlikely(preempt_count_dec_and_test())) \
		__preempt_schedule(); \
} while (0)

asmlinkage __visible void __sched notrace preempt_schedule(void)
{
	/*
	 * If there is a non-zero preempt_count or interrupts are disabled,
	 * we do not want to preempt the current task. Just return..
	 */
	if (likely(!preemptible()))
		return;

	preempt_schedule_common();
}
NOKPROBE_SYMBOL(preempt_schedule);
EXPORT_SYMBOL(preempt_schedule);

#define preemptible()	(preempt_count() == 0 && !irqs_disabled())

Shadow-box는 태스크 목록을 관찰할 때 스핀락을 사용하며, 커널 모듈 목록을 관찰할 때 뮤텍스를 어쩔 수 없이 사용합니다. 이 두 자료구조는 리스트 형태로 되어 있는데, 리스트를 순회하는 동안 리스트가 바뀌어 엉뚱한 곳에 데이터를 읽어오거나 접근하거나 하면 안 되기 때문이죠. ^^;;;

결국 해결책을 찾아야 했는데요, 스핀락 같은 경우는 인터럽트가 비활성화되어 있으면 스케줄러를 호출하지 않기 때문에 별 문제가 없는 것으로 확인되었습니다. 다만, 뮤텍스 같은 경우는 잘못하면 호스트에서 게스트의 다른 프로세스를 실행할 수 있는 잠재적인 문제가 있어서 결국 제거하는 방법을 택했습니다. 이로 인해 일부 탐지 기능이 지연되는 문제가 있지만... 안정성이 먼저니까요. ^^;;;

그럼 좋은 밤 되세요 ^^)/

+ Recent posts