diff --git a/fs/rc/jobs/job.go b/fs/rc/jobs/job.go index e1acfae4c..8f5d365a1 100644 --- a/fs/rc/jobs/job.go +++ b/fs/rc/jobs/job.go @@ -108,7 +108,11 @@ func (job *Job) OnFinish(fn func()) func() { func (job *Job) run(ctx context.Context, fn rc.Func, in rc.Params) { defer func() { if r := recover(); r != nil { - job.finish(nil, fmt.Errorf("panic received: %v \n%s", r, string(debug.Stack()))) + // Log the full stack trace server-side only - it must not + // be returned to the rc caller as it leaks internal paths, + // dependency versions and memory addresses. + fs.Errorf(nil, "rc: job %d panic: %v\n%s", job.ID, r, string(debug.Stack())) + job.finish(nil, fmt.Errorf("panic received: %v", r)) } }() job.finish(fn(ctx, in)) diff --git a/fs/rc/jobs/job_test.go b/fs/rc/jobs/job_test.go index 02f171860..424e2d4d5 100644 --- a/fs/rc/jobs/job_test.go +++ b/fs/rc/jobs/job_test.go @@ -234,6 +234,10 @@ func TestJobRunPanic(t *testing.T) { assert.Equal(t, rc.Params{}, job.Output) assert.True(t, job.Duration >= floatSleepTime) assert.Contains(t, job.Error, "panic received: boom") + // The stack trace must not be leaked to the rc caller - it is + // logged server-side only. + assert.NotContains(t, job.Error, "goroutine") + assert.NotContains(t, job.Error, "runtime/debug.Stack") assert.Equal(t, false, job.Success) assert.Equal(t, true, job.Finished) job.mu.Unlock()