作者:wzt
原文鏈接:https://mp.weixin.qq.com/s/5MFY-Y0uOxtfpFO0Z3SEVw
1 簡介
Freebsd的jail模型是一個純粹的沙箱模型,用來限制進程的一些行為,是一種安全機制。它是一種簡單的“虛擬化”設計,更精確的說它只是簡單的namespace機制實現。Linux的容器機制技術包括pid namespace、ipc namespace、uts namespace、mount namespace、netstack namespace、cgroup等。Freebsd的jail只包含了uts、netstack的namespace完整實現,剩下的namespace不具備功能完整性,只做了一些簡單的隔離限制,在后面的源碼分析中,將會看到詳細的闡述。
2 架構
2.1 Jail模型
Jail模型在內核里是以prison為單位進行管理, 一個prison有其父prison,有兄弟prison,有子prison,很類似進程的關系鏈。
prison用結構體struct prison表示, 內核使用宏FOREACH_PRISON_DESCENDANT遍歷prison:
sys/jail.h:
#define FOREACH_PRISON_DESCENDANT(ppr, cpr, descend) \
for ((cpr) = (ppr), (descend) = 1; \
((cpr) = (((descend) && !LIST_EMPTY(&(cpr)->pr_children)) \
? LIST_FIRST(&(cpr)->pr_children) \
: ((cpr) == (ppr) \
? NULL \
: (((descend) = LIST_NEXT(cpr, pr_sibling) != NULL) \
? LIST_NEXT(cpr, pr_sibling) \
: (cpr)->pr_parent))));) \
if (!(descend)) \
; \
else
這個宏讀起來比較繞,首先對prison的子節點進行深度遍歷,然后在對其兄弟prison進行遍歷。descend只起到一個標識作用,在從children向上回溯時不需要在做檢查。
Freebsd增加了sys_jail、sys_jail_set、jail_remove三個系統調用,來支持jail的建立和銷毀。Jail在內核中的結構如下:
struct jail {
uint32_t version;// jail版本號
char *path;// chroot路徑
char *hostname;// 主機名
char *jailname;// jail名
uint32_t ip4s;// ipv4地址數目
uint32_t ip6s;// ipv6地址數目
struct in_addr *ip4;// ipv4地址數組
struct in6_addr *ip6;// ipv6地址數組
};
kern_jail_set函數用于將struct jail結構轉化為struct prison結構,后者掛接于每個線程的struct ucred結構體里。
struct prison {
...
char pr_hostname[MAXHOSTNAMELEN]; /* (p) jail hostname */
char pr_domainname[MAXHOSTNAMELEN]; /* (p) jail domainname */
char pr_hostuuid[HOSTUUIDLEN]; /* (p) jail hostuuid */
char pr_osrelease[OSRELEASELEN]; /* (c) kern.osrelease value */
};
以后內核對于取主機名等操作就從此結構體里取出,這樣就實現了類似linux uts namespace的機制。
系統的init進程處于prison0之中,類似于linux的init進程處于init_namespace之中。
2.2 進程隔離
linux提供了pid namespace機制,不同的pid namespace之間不能進行信號發送,同一個pid namespace組里,只有父pid namespace可以給子pid namespace發送信號,反過來則不行。比如父進程可以發送kill -9殺死子進程,子進程卻不能殺死父進程,這是進程間隔離的基本要素。Freebsd的實現也是如此。
sys_kill->p_cansignal->cr_cansignal->prison_check->prison_ischild
int
prison_ischild(struct prison *pr1, struct prison *pr2)
{
for (pr2 = pr2->pr_parent; pr2 != NULL; pr2 = pr2->pr_parent)
if (pr1 == pr2)
return (1);
return (0);
}
當發送信號操作時,會調用prison_ischild函數檢查目標進程是否處于當前進程的子prison里,如果是則繼續發送信號,如果不是則失敗返回。這個算法與linux的pid namespace機制如出一轍,只不過linux的每個pid namespace都有自己的進程號管理體系,它通過層次來實現進程的pid namespace區分,假如一個進程處于三層namespace中, 那么在它的一層、二層namespace中都占有一個獨立的進程號,而Freebsd并沒有采取上述機制。
2.3 ipc隔離
在ipc相關的通訊中,也有類似進程隔離的檢查機制。
kern/sysv_msg.c
sys_msgctl->kern_msgctl->msq_prison_cansee
static int
msq_prison_cansee(struct prison *rpr, struct msqid_kernel *msqkptr)
{
if (msqkptr->cred == NULL ||
!(rpr == msqkptr->cred->cr_prison ||
prison_ischild(rpr, msqkptr->cred->cr_prison)))
return (EINVAL);
return (0);
}
msq_prison_cansee仍然調用prison_ischild函數來判斷目標進程是否處于當前進程的子prison中。
kern/sysv_shm.c:
sys_shmat->kern_shmat->kern_shmctl_locked->shm_find_segment->shm_prison_cansee
static int
shm_prison_cansee(struct prison *rpr, struct shmid_kernel *shmseg)
{
if (shmseg->cred == NULL ||
!(rpr == shmseg->cred->cr_prison ||
prison_ischild(rpr, shmseg->cred->cr_prison)))
return (EINVAL);
return (0);
}
kern/sysv_sem.c:
sys___semctl->kern_semctl->sem_prison_cansee
static int
sem_prison_cansee(struct prison *rpr, struct semid_kernel *semakptr)
{
if (semakptr->cred == NULL ||
!(rpr == semakptr->cred->cr_prison ||
prison_ischild(rpr, semakptr->cred->cr_prison)))
return (EINVAL);
return (0);
}
2.4 文件系統隔離
2.4.1 mount隔離
freebsd沒有實現linux的mount namespace機制,而是在mount的操作中,判斷是否處于prison的進程可以執行mount操作。
kern/vfs_subr.c:
int
vfs_suser(struct mount *mp, struct thread *td)
{
int error;
if (jailed(td->td_ucred)) {
if (prison_check(td->td_ucred, mp->mnt_cred) != 0)
return (EPERM);
}
}
當進程處于jail中,就調用prison_check對當前進程的cred和要掛接的目錄cred進行比對,后者又調用prison_ischild來判斷要掛接的目錄是否在當前prison之外,如果是的話就不允許此操作。
2.4.2 文件系統屬性修改
ufs/ufs/ufs_vnops.c:
static int
ufs_setattr(ap)
struct vop_setattr_args /* {
struct vnode *a_vp;
struct vattr *a_vap;
struct ucred *a_cred;
} */ *ap;
{
if (!priv_check_cred(cred, PRIV_VFS_SYSFLAGS)) {
}
判斷當前處在prison中的進程是否有PRIV_VFS_SYSFLAGS修改文件系統屬性的權限。
priv_check_cred->prison_priv_check
int
prison_priv_check(struct ucred *cred, int priv)
{
case PRIV_VFS_SYSFLAGS:
if (cred->cr_prison->pr_allow & PR_ALLOW_CHFLAGS)
return (0);
}
2.5 網絡隔離
freebsd的net stack namespace是通過vnet模型來實現的,jail模型只做簡單的隔離操作,比如是否允許prison的進程綁定地址等等。
sys_bind->kern_bindat->sobind
int
sobind(struct socket *so, struct sockaddr *nam, struct thread *td)
{
error = (*so->so_proto->pr_usrreqs->pru_bind)(so, nam, td);
}
so->so_proto->pr_usrreqs->pru_bind指向某個具體的地址族中的某個協議地址綁定操作指針,我們以INET地址族TCP協議的端口綁定操作來看下:
in_pcbbind->in_pcbbind_setup
int
in_pcbbind_setup(struct inpcb *inp, struct sockaddr *nam, in_addr_t *laddrp,
u_short *lportp, struct ucred *cred)
{
if ((error = prison_local_ip4(cred, &laddr)) != 0)
}
Tcp bind的初始化操作中調用prison_local_ip4來判斷是否允許此次bind操作。
int
prison_local_ip4(struct ucred *cred, struct in_addr *ia)
{
pr = cred->cr_prison;
if (!(pr->pr_flags & PR_IP4)) [1]
return (0);
mtx_lock(&pr->pr_mtx);
if (!(pr->pr_flags & PR_IP4)) {
mtx_unlock(&pr->pr_mtx);
return (0);
}
if (pr->pr_ip4 == NULL) { [2]
mtx_unlock(&pr->pr_mtx);
return (EAFNOSUPPORT);
}
ia0.s_addr = ntohl(ia->s_addr);
if (ia0.s_addr == INADDR_ANY) { [3]
if (pr->pr_ip4s == 1)
ia->s_addr = pr->pr_ip4[0].s_addr;
mtx_unlock(&pr->pr_mtx);
return (0);
}
error = prison_check_ip4_locked(pr, ia); [4]
if (error == EADDRNOTAVAIL && ia0.s_addr == INADDR_LOOPBACK) {
ia->s_addr = pr->pr_ip4[0].s_addr;
error = 0;
}
mtx_unlock(&pr->pr_mtx);
return (error);
}
[1] 如果prison沒有設置PR_IP4標志則放行通過, 如果設置了但pr->pr_ip4保存的ipv4地址數組為空,則失敗返回。在[3]處,如果地址類型為INADDR_ANY,則放行通過。然后調用[4]處的prison_check_ip4_locked判斷要綁定的地址是否在prison ipv4地址數組中,它通過二分法來做匹配。如果綁定的地址不在其中,則返回失敗,也就是說prison中進程使用socket綁定的ipv4地址必須是在通過sys_jail設置好的地址范圍。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1449/
暫無評論