diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2018-07-29 19:46:05 +0200 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2018-07-29 19:46:05 +0200 |
| commit | 1a381291a38fbca36a7fd1ca142e4cee7794e3b1 (patch) | |
| tree | 52400082d430390e32f998a491a62c28d213d2c7 | |
| parent | 0824d7a1bfdf50e4ab99fd27cc5fc55cb620b1ab (diff) | |
executor: collect coverage from unfinished syscalls
Write coverage from unfinished syscalls.
Also detect when a syscall was blocked during execution,
even if it finished. Helpful for fallback coverage.
Fixes #580
| -rw-r--r-- | executor/executor.cc | 275 | ||||
| -rw-r--r-- | pkg/ipc/ipc.go | 51 | ||||
| -rw-r--r-- | prog/analysis.go | 33 | ||||
| -rw-r--r-- | syz-fuzzer/proc.go | 2 |
4 files changed, 215 insertions, 146 deletions
diff --git a/executor/executor.cc b/executor/executor.cc index c5b9efe42..b7ec949d0 100644 --- a/executor/executor.cc +++ b/executor/executor.cc @@ -184,7 +184,7 @@ struct thread_t { uint64* copyout_pos; uint64 copyout_index; bool colliding; - bool handled; + bool executing; int call_index; int call_num; int num_args; @@ -195,7 +195,8 @@ struct thread_t { cover_t cov; }; -thread_t threads[kMaxThreads]; +static thread_t threads[kMaxThreads]; +static thread_t* last_scheduled; struct res_t { bool executed; @@ -233,12 +234,18 @@ struct execute_reply { uint32 status; }; +// call_reply.flags +const uint32 call_flag_executed = 1 << 0; +const uint32 call_flag_finished = 1 << 1; +const uint32 call_flag_blocked = 1 << 2; +const uint32 call_flag_fault_injected = 1 << 3; + struct call_reply { execute_reply header; uint32 call_index; uint32 call_num; uint32 reserrno; - uint32 fault_injected; + uint32 flags; uint32 signal_size; uint32 cover_size; uint32 comps_size; @@ -269,6 +276,8 @@ struct kcov_comparison_t { static thread_t* schedule_call(int call_index, int call_num, bool colliding, uint64 copyout_index, uint64 num_args, uint64* args, uint64* pos); static void handle_completion(thread_t* th); +static void copyout_call_results(thread_t* th); +static void write_call_output(thread_t* th, bool finished); static void execute_call(thread_t* th); static void thread_create(thread_t* th, int id); static void* worker_thread(void* arg); @@ -625,38 +634,23 @@ retry: args[i] = read_arg(&input_pos); for (uint64 i = num_args; i < 6; i++) args[i] = 0; - thread_t* th = schedule_call(call_index++, call_num, colliding, copyout_index, num_args, args, input_pos); + thread_t* th = schedule_call(call_index++, call_num, colliding, copyout_index, + num_args, args, input_pos); if (colliding && (call_index % 2) == 0) { // Don't wait for every other call. // We already have results from the previous execution. } else if (flag_threaded) { // Wait for call completion. - // Note: sys knows about this 25ms timeout when it generates - // timespec/timeval values. - const uint64 timeout_ms = flag_debug ? 3000 : 25; + // Note: sys knows about this 25ms timeout when it generates timespec/timeval values. + const uint64 timeout_ms = flag_debug ? 1000 : 25; if (event_timedwait(&th->done, timeout_ms)) handle_completion(th); // Check if any of previous calls have completed. - // Give them some additional time, because they could have been - // just unblocked by the current call. - if (running < 0) - fail("running = %d", running); - if (running > 0) { - bool last = read_input(&input_pos, true) == instr_eof; - uint64 wait = last ? 100 : 2; - uint64 wait_start = current_time_ms(); - uint64 wait_end = wait_start + wait; - if (!colliding && wait_end < start + 800) - wait_end = start + 800; - while (running > 0 && current_time_ms() <= wait_end) { - sleep_ms(1); - for (int i = 0; i < kMaxThreads; i++) { - th = &threads[i]; - if (!th->handled && event_isset(&th->done)) - handle_completion(th); - } - } + for (int i = 0; i < kMaxThreads; i++) { + th = &threads[i]; + if (th->executing && event_isset(&th->done)) + handle_completion(th); } } else { // Execute directly. @@ -669,6 +663,33 @@ retry: } } + if (!colliding && !collide && running > 0) { + // Give unfinished syscalls some additional time. + uint64 wait = 100; + uint64 wait_start = current_time_ms(); + uint64 wait_end = wait_start + wait; + if (wait_end < start + 800) + wait_end = start + 800; + while (running > 0 && current_time_ms() <= wait_end) { + sleep_ms(1); + for (int i = 0; i < kMaxThreads; i++) { + thread_t* th = &threads[i]; + if (th->executing && event_isset(&th->done)) + handle_completion(th); + } + } + // Write output coverage for unfinished calls. + if (flag_cover && running > 0) { + for (int i = 0; i < kMaxThreads; i++) { + thread_t* th = &threads[i]; + if (th->executing) { + cover_collect(&th->cov); + write_call_output(th, false); + } + } + } + } + if (flag_collide && !flag_inject_fault && !colliding && !collide) { debug("enabling collider\n"); collide = colliding = true; @@ -685,7 +706,7 @@ thread_t* schedule_call(int call_index, int call_num, bool colliding, uint64 cop if (!th->created) thread_create(th, i); if (event_isset(&th->done)) { - if (!th->handled) + if (th->executing) handle_completion(th); break; } @@ -694,14 +715,15 @@ thread_t* schedule_call(int call_index, int call_num, bool colliding, uint64 cop exitf("out of threads"); thread_t* th = &threads[i]; debug("scheduling call %d [%s] on thread %d\n", call_index, syscalls[call_num].name, th->id); - if (event_isset(&th->ready) || !event_isset(&th->done) || !th->handled) - fail("bad thread state in schedule: ready=%d done=%d handled=%d", - event_isset(&th->ready), event_isset(&th->done), th->handled); + if (event_isset(&th->ready) || !event_isset(&th->done) || th->executing) + fail("bad thread state in schedule: ready=%d done=%d executing=%d", + event_isset(&th->ready), event_isset(&th->done), th->executing); + last_scheduled = th; th->colliding = colliding; th->copyout_pos = pos; th->copyout_index = copyout_index; event_reset(&th->done); - th->handled = false; + th->executing = true; th->call_index = call_index; th->call_num = call_num; th->num_args = num_args; @@ -757,105 +779,124 @@ void write_coverage_signal(thread_t* th, uint32* signal_count_pos, uint32* cover void handle_completion(thread_t* th) { debug("completion of call %d [%s] on thread %d\n", th->call_index, syscalls[th->call_num].name, th->id); - if (event_isset(&th->ready) || !event_isset(&th->done) || th->handled) - fail("bad thread state in completion: ready=%d done=%d handled=%d", - event_isset(&th->ready), event_isset(&th->done), th->handled); - if (th->res != (long)-1) { - if (th->copyout_index != no_copyout) { - if (th->copyout_index >= kMaxCommands) - fail("result idx %lld overflows kMaxCommands", th->copyout_index); - results[th->copyout_index].executed = true; - results[th->copyout_index].val = th->res; - } - for (bool done = false; !done;) { - uint64 instr = read_input(&th->copyout_pos); - switch (instr) { - case instr_copyout: { - uint64 index = read_input(&th->copyout_pos); - if (index >= kMaxCommands) - fail("result idx %lld overflows kMaxCommands", index); - char* addr = (char*)read_input(&th->copyout_pos); - uint64 size = read_input(&th->copyout_pos); - uint64 val = 0; - if (copyout(addr, size, &val)) { - results[index].executed = true; - results[index].val = val; - } - debug("copyout 0x%llx from %p\n", val, addr); - break; - } - default: - done = true; - break; + if (event_isset(&th->ready) || !event_isset(&th->done) || !th->executing) + fail("bad thread state in completion: ready=%d done=%d executing=%d", + event_isset(&th->ready), event_isset(&th->done), th->executing); + if (th->res != (long)-1) + copyout_call_results(th); + if (!collide && !th->colliding) + write_call_output(th, true); + th->executing = false; + running--; + if (running < 0) + fail("running = %d", running); +} + +void copyout_call_results(thread_t* th) +{ + if (th->copyout_index != no_copyout) { + if (th->copyout_index >= kMaxCommands) + fail("result idx %lld overflows kMaxCommands", th->copyout_index); + results[th->copyout_index].executed = true; + results[th->copyout_index].val = th->res; + } + for (bool done = false; !done;) { + uint64 instr = read_input(&th->copyout_pos); + switch (instr) { + case instr_copyout: { + uint64 index = read_input(&th->copyout_pos); + if (index >= kMaxCommands) + fail("result idx %lld overflows kMaxCommands", index); + char* addr = (char*)read_input(&th->copyout_pos); + uint64 size = read_input(&th->copyout_pos); + uint64 val = 0; + if (copyout(addr, size, &val)) { + results[index].executed = true; + results[index].val = val; } + debug("copyout 0x%llx from %p\n", val, addr); + break; + } + default: + done = true; + break; } } - if (!collide && !th->colliding) { - uint32 reserrno = th->res != -1 ? 0 : th->reserrno; +} + +void write_call_output(thread_t* th, bool finished) +{ + uint32 reserrno = 999; + uint32 call_flags = call_flag_executed; + const bool blocked = th != last_scheduled; + if (finished) { + reserrno = th->res != -1 ? 0 : th->reserrno; + call_flags |= call_flag_finished | + (blocked ? call_flag_blocked : 0) | + (th->fault_injected ? call_flag_fault_injected : 0); + } #if SYZ_EXECUTOR_USES_SHMEM - write_output(th->call_index); - write_output(th->call_num); - write_output(reserrno); - write_output(th->fault_injected); - uint32* signal_count_pos = write_output(0); // filled in later - uint32* cover_count_pos = write_output(0); // filled in later - uint32* comps_count_pos = write_output(0); // filled in later - - if (flag_collect_comps) { - // Collect only the comparisons - uint32 ncomps = th->cov.size; - kcov_comparison_t* start = (kcov_comparison_t*)(th->cov.data + sizeof(uint64)); - kcov_comparison_t* end = start + ncomps; - if ((char*)end > th->cov.data_end) - fail("too many comparisons %u", ncomps); - std::sort(start, end); - ncomps = std::unique(start, end) - start; - uint32 comps_size = 0; - for (uint32 i = 0; i < ncomps; ++i) { - if (start[i].ignore()) - continue; - comps_size++; - start[i].write(); - } - // Write out number of comparisons. - *comps_count_pos = comps_size; - } else if (flag_cover) { - if (is_kernel_64_bit) - write_coverage_signal<uint64>(th, signal_count_pos, cover_count_pos); - else - write_coverage_signal<uint32>(th, signal_count_pos, cover_count_pos); + write_output(th->call_index); + write_output(th->call_num); + write_output(reserrno); + write_output(call_flags); + uint32* signal_count_pos = write_output(0); // filled in later + uint32* cover_count_pos = write_output(0); // filled in later + uint32* comps_count_pos = write_output(0); // filled in later + + if (flag_collect_comps) { + // Collect only the comparisons + uint32 ncomps = th->cov.size; + kcov_comparison_t* start = (kcov_comparison_t*)(th->cov.data + sizeof(uint64)); + kcov_comparison_t* end = start + ncomps; + if ((char*)end > th->cov.data_end) + fail("too many comparisons %u", ncomps); + std::sort(start, end); + ncomps = std::unique(start, end) - start; + uint32 comps_size = 0; + for (uint32 i = 0; i < ncomps; ++i) { + if (start[i].ignore()) + continue; + comps_size++; + start[i].write(); } - debug("out #%u: index=%u num=%u errno=%d sig=%u cover=%u comps=%u\n", - completed, th->call_index, th->call_num, reserrno, - *signal_count_pos, *cover_count_pos, *comps_count_pos); - completed++; - write_completed(completed); + // Write out number of comparisons. + *comps_count_pos = comps_size; + } else if (flag_cover) { + if (is_kernel_64_bit) + write_coverage_signal<uint64>(th, signal_count_pos, cover_count_pos); + else + write_coverage_signal<uint32>(th, signal_count_pos, cover_count_pos); + } + debug("out #%u: index=%u num=%u errno=%d finished=%d blocked=%d sig=%u cover=%u comps=%u\n", + completed, th->call_index, th->call_num, reserrno, finished, blocked, + *signal_count_pos, *cover_count_pos, *comps_count_pos); + completed++; + write_completed(completed); #else - call_reply reply; - reply.header.magic = kOutMagic; - reply.header.done = 0; - reply.header.status = 0; - reply.call_index = th->call_index; - reply.call_num = th->call_num; - reply.reserrno = reserrno; - reply.fault_injected = th->fault_injected; - reply.signal_size = 0; - reply.cover_size = 0; - reply.comps_size = 0; - if (write(kOutPipeFd, &reply, sizeof(reply)) != sizeof(reply)) - fail("control pipe call write failed"); - debug("out: index=%u num=%u errno=%d\n", th->call_index, th->call_num, reserrno); + call_reply reply; + reply.header.magic = kOutMagic; + reply.header.done = 0; + reply.header.status = 0; + reply.call_index = th->call_index; + reply.call_num = th->call_num; + reply.reserrno = reserrno; + reply.flags = call_flags; + reply.signal_size = 0; + reply.cover_size = 0; + reply.comps_size = 0; + if (write(kOutPipeFd, &reply, sizeof(reply)) != sizeof(reply)) + fail("control pipe call write failed"); + debug("out: index=%u num=%u errno=%d finished=%d blocked=%d\n", + th->call_index, th->call_num, reserrno, finished, blocked); #endif - } - th->handled = true; - running--; } void thread_create(thread_t* th, int id) { th->created = true; th->id = id; - th->handled = true; + th->executing = false; event_init(&th->ready); event_init(&th->done); event_set(&th->done); diff --git a/pkg/ipc/ipc.go b/pkg/ipc/ipc.go index 167bca893..283b8130f 100644 --- a/pkg/ipc/ipc.go +++ b/pkg/ipc/ipc.go @@ -152,14 +152,22 @@ func DefaultConfig(target *prog.Target) (*Config, *ExecOpts, error) { return c, opts, nil } +type CallFlags uint32 + +const ( + CallExecuted CallFlags = 1 << iota // was started at all + CallFinished // finished executing (rather than blocked forever) + CallBlocked // finished but blocked during execution + CallFaultInjected // fault was injected into this call +) + type CallInfo struct { - Executed bool - Signal []uint32 // feedback signal, filled if FlagSignal is set - Cover []uint32 // per-call coverage, filled if FlagSignal is set and cover == true, + Flags CallFlags + Signal []uint32 // feedback signal, filled if FlagSignal is set + Cover []uint32 // per-call coverage, filled if FlagSignal is set and cover == true, //if dedup == false, then cov effectively contains a trace, otherwise duplicates are removed - Comps prog.CompMap // per-call comparison operands - Errno int // call errno (0 if the call was successful) - FaultInjected bool + Comps prog.CompMap // per-call comparison operands + Errno int // call errno (0 if the call was successful) } type Env struct { @@ -326,7 +334,15 @@ func (env *Env) Exec(opts *ExecOpts, p *prog.Prog) (output []byte, info []CallIn func addFallbackSignal(p *prog.Prog, info []CallInfo) { callInfos := make([]prog.CallInfo, len(info)) for i, inf := range info { - callInfos[i].Executed = inf.Executed + if inf.Flags&CallExecuted != 0 { + callInfos[i].Flags |= prog.CallExecuted + } + if inf.Flags&CallFinished != 0 { + callInfos[i].Flags |= prog.CallFinished + } + if inf.Flags&CallBlocked != 0 { + callInfos[i].Flags |= prog.CallBlocked + } callInfos[i].Errno = inf.Errno } p.FallbackSignal(callInfos) @@ -385,9 +401,9 @@ func (env *Env) readOutCoverage(p *prog.Prog) (info []CallInfo, err0 error) { return buf.String() } for i := uint32(0); i < ncmd; i++ { - var callIndex, callNum, errno, faultInjected, signalSize, coverSize, compsSize uint32 + var callIndex, callNum, errno, callFlags, signalSize, coverSize, compsSize uint32 if !readOut(&callIndex) || !readOut(&callNum) || !readOut(&errno) || - !readOut(&faultInjected) || !readOut(&signalSize) || + !readOut(&callFlags) || !readOut(&signalSize) || !readOut(&coverSize) || !readOut(&compsSize) { err0 = fmt.Errorf("executor %v: failed to read output coverage", env.pid) return @@ -398,7 +414,6 @@ func (env *Env) readOutCoverage(p *prog.Prog) (info []CallInfo, err0 error) { return } c := p.Calls[callIndex] - info[callIndex].Executed = true if num := c.Meta.ID; uint32(num) != callNum { err0 = fmt.Errorf("executor %v: failed to read output coverage:"+ " record %v call %v: expect syscall %v, got %v, executed %v (cov: %v)", @@ -411,7 +426,7 @@ func (env *Env) readOutCoverage(p *prog.Prog) (info []CallInfo, err0 error) { return } info[callIndex].Errno = int(errno) - info[callIndex].FaultInjected = faultInjected != 0 + info[callIndex].Flags = CallFlags(callFlags) if signalSize > uint32(len(out)) { err0 = fmt.Errorf("executor %v: failed to read output signal: record %v, call %v, signalsize=%v coversize=%v", env.pid, i, callIndex, signalSize, coverSize) @@ -529,13 +544,13 @@ type executeReply struct { // gometalinter complains about unused fields // nolint type callReply struct { - callIndex uint32 - callNum uint32 - errno uint32 - faultInjected uint32 - signalSize uint32 - coverSize uint32 - compsSize uint32 + callIndex uint32 + callNum uint32 + errno uint32 + flags uint32 // see CallFlags + signalSize uint32 + coverSize uint32 + compsSize uint32 // signal/cover/comps follow } diff --git a/prog/analysis.go b/prog/analysis.go index 3da9cba01..f75459783 100644 --- a/prog/analysis.go +++ b/prog/analysis.go @@ -171,28 +171,41 @@ func RequiredFeatures(p *Prog) (bitmasks, csums bool) { return } +type CallFlags int + +const ( + CallExecuted CallFlags = 1 << iota // was started at all + CallFinished // finished executing (rather than blocked forever) + CallBlocked // finished but blocked during execution +) + type CallInfo struct { - Executed bool - Errno int - Signal []uint32 + Flags CallFlags + Errno int + Signal []uint32 } const ( fallbackSignalErrno = iota + fallbackSignalErrnoBlocked fallbackSignalCtor fallbackSignalFlags - fallbackCallMask = 0x3fff + fallbackCallMask = 0x1fff ) func (p *Prog) FallbackSignal(info []CallInfo) { resources := make(map[*ResultArg]*Call) for i, c := range p.Calls { inf := &info[i] - if !inf.Executed { + if inf.Flags&CallExecuted == 0 { continue } id := c.Meta.ID - inf.Signal = append(inf.Signal, encodeFallbackSignal(fallbackSignalErrno, id, inf.Errno)) + typ := fallbackSignalErrno + if inf.Flags&CallFinished != 0 && inf.Flags&CallBlocked != 0 { + typ = fallbackSignalErrnoBlocked + } + inf.Signal = append(inf.Signal, encodeFallbackSignal(typ, id, inf.Errno)) if inf.Errno != 0 { continue } @@ -261,7 +274,7 @@ func (p *Prog) FallbackSignal(info []CallInfo) { func DecodeFallbackSignal(s uint32) (callID, errno int) { typ, id, aux := decodeFallbackSignal(s) switch typ { - case fallbackSignalErrno: + case fallbackSignalErrno, fallbackSignalErrnoBlocked: return id, aux case fallbackSignalCtor, fallbackSignalFlags: return id, 0 @@ -271,15 +284,15 @@ func DecodeFallbackSignal(s uint32) (callID, errno int) { } func encodeFallbackSignal(typ, id, aux int) uint32 { - if typ & ^3 != 0 { + if typ & ^7 != 0 { panic(fmt.Sprintf("bad fallback signal type %v", typ)) } if id & ^fallbackCallMask != 0 { panic(fmt.Sprintf("bad call id in fallback signal %v", id)) } - return uint32(typ) | uint32(id&0x3fff)<<2 | uint32(aux)<<16 + return uint32(typ) | uint32(id&fallbackCallMask)<<3 | uint32(aux)<<16 } func decodeFallbackSignal(s uint32) (typ, id, aux int) { - return int(s & 3), int((s >> 2) & fallbackCallMask), int(s >> 16) + return int(s & 7), int((s >> 3) & fallbackCallMask), int(s >> 16) } diff --git a/syz-fuzzer/proc.go b/syz-fuzzer/proc.go index c3499fcbe..a217c9735 100644 --- a/syz-fuzzer/proc.go +++ b/syz-fuzzer/proc.go @@ -207,7 +207,7 @@ func (proc *Proc) failCall(p *prog.Prog, call int) { opts.FaultCall = call opts.FaultNth = nth info := proc.executeRaw(&opts, p, StatSmash) - if info != nil && len(info) > call && !info[call].FaultInjected { + if info != nil && len(info) > call && info[call].Flags&ipc.CallFaultInjected == 0 { break } } |
