Add YAMA security module restrictions on ptrace(2).

Restrict ptrace(2) according to the default configurations of the YAMA security
module (mode 1), which is a common default among various Linux distributions.
The new access checks only permit the tracer to proceed if one of the following
conditions is met:

a) The tracer is already attached to the tracee.

b) The target is a descendant of the tracer.

c) The target has explicitly given permission to the tracer through the
PR_SET_PTRACER prctl.

d) The tracer has CAP_SYS_PTRACE.

See security/yama/yama_lsm.c for more details.

Note that these checks are added to CanTrace, which is checked for
PTRACE_ATTACH as well as some other operations, e.g., checking a process'
memory layout through /proc/[pid]/mem.

Since this patch adds restrictions to ptrace, it may break compatibility for
applications run by non-root users that, for instance, rely on being able to
trace processes that are not descended from the tracer (e.g., `gdb -p`). YAMA
restrictions can be turned off by setting /proc/sys/kernel/yama/ptrace_scope
to 0, or exceptions can be made on a per-process basis with the PR_SET_PTRACER
prctl.

Reported-by: syzbot+622822d8bca08c99e8c8@syzkaller.appspotmail.com
PiperOrigin-RevId: 359237723
This commit is contained in:
Dean Deng
2021-02-24 02:03:16 -08:00
committed by gVisor bot
parent 6e000d3424
commit acd516cfe2
13 changed files with 1447 additions and 20 deletions
+5
View File
@@ -144,6 +144,11 @@ const (
// PR_MPX_DISABLE_MANAGEMENT disables kernel management of Memory
// Protection eXtensions (MPX) bounds tables.
PR_MPX_DISABLE_MANAGEMENT = 44
// PR_SET_PTRACER allows a specific process (or any, if PR_SET_PTRACER_ANY is
// specified) to ptrace the current task.
PR_SET_PTRACER = 0x59616d61
PR_SET_PTRACER_ANY = -1
)
// From <asm/prctl.h>
+6
View File
@@ -87,3 +87,9 @@ const (
PTRACE_O_EXITKILL = 1 << 20
PTRACE_O_SUSPEND_SECCOMP = 1 << 21
)
// YAMA ptrace_scope levels from security/yama/yama_lsm.c.
const (
YAMA_SCOPE_DISABLED = 0
YAMA_SCOPE_RELATIONAL = 1
)
+1
View File
@@ -75,6 +75,7 @@ go_library(
"tasks_files.go",
"tasks_inode_refs.go",
"tasks_sys.go",
"yama.go",
],
visibility = ["//pkg/sentry:internal"],
deps = [
+3
View File
@@ -48,6 +48,9 @@ func (fs *filesystem) newSysDir(ctx context.Context, root *auth.Credentials, k *
"shmall": fs.newInode(ctx, root, 0444, shmData(linux.SHMALL)),
"shmmax": fs.newInode(ctx, root, 0444, shmData(linux.SHMMAX)),
"shmmni": fs.newInode(ctx, root, 0444, shmData(linux.SHMMNI)),
"yama": fs.newStaticDir(ctx, root, map[string]kernfs.Inode{
"ptrace_scope": fs.newYAMAPtraceScopeFile(ctx, k, root),
}),
}),
"vm": fs.newStaticDir(ctx, root, map[string]kernfs.Inode{
"mmap_min_addr": fs.newInode(ctx, root, 0444, &mmapMinAddrData{k: k}),
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2021 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proc
import (
"bytes"
"fmt"
"sync/atomic"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/syserror"
"gvisor.dev/gvisor/pkg/usermem"
)
func (fs *filesystem) newYAMAPtraceScopeFile(ctx context.Context, k *kernel.Kernel, creds *auth.Credentials) kernfs.Inode {
s := &yamaPtraceScope{level: &k.YAMAPtraceScope}
s.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), s, 0644)
return s
}
// yamaPtraceScope implements vfs.WritableDynamicBytesSource for
// /sys/kernel/yama/ptrace_scope.
//
// +stateify savable
type yamaPtraceScope struct {
kernfs.DynamicBytesFile
// level is the ptrace_scope level.
level *int32
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (s *yamaPtraceScope) Generate(ctx context.Context, buf *bytes.Buffer) error {
_, err := fmt.Fprintf(buf, "%d\n", atomic.LoadInt32(s.level))
return err
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (s *yamaPtraceScope) Write(ctx context.Context, src usermem.IOSequence, offset int64) (int64, error) {
if offset != 0 {
// Ignore partial writes.
return 0, syserror.EINVAL
}
if src.NumBytes() == 0 {
return 0, nil
}
// Limit the amount of memory allocated.
src = src.TakeFirst(usermem.PageSize - 1)
var v int32
n, err := usermem.CopyInt32StringInVec(ctx, src.IO, src.Addrs, &v, src.Opts)
if err != nil {
return 0, err
}
// We do not support YAMA levels > YAMA_SCOPE_RELATIONAL.
if v < linux.YAMA_SCOPE_DISABLED || v > linux.YAMA_SCOPE_RELATIONAL {
return 0, syserror.EINVAL
}
atomic.StoreInt32(s.level, v)
return n, nil
}
+14 -1
View File
@@ -282,6 +282,18 @@ type Kernel struct {
// If set to true, report address space activation waits as if the task is in
// external wait so that the watchdog doesn't report the task stuck.
SleepForAddressSpaceActivation bool
// Exceptions to YAMA ptrace restrictions. Each key-value pair represents a
// tracee-tracer relationship. The key is a process (technically, the thread
// group leader) that can be traced by any thread that is a descendant of the
// value. If the value is nil, then anyone can trace the process represented by
// the key.
//
// ptraceExceptions is protected by the TaskSet mutex.
ptraceExceptions map[*Task]*Task
// YAMAPtraceScope is the current level of YAMA ptrace restrictions.
YAMAPtraceScope int32
}
// InitKernelArgs holds arguments to Init.
@@ -382,6 +394,8 @@ func (k *Kernel) Init(args InitKernelArgs) error {
k.monotonicClock = &timekeeperClock{tk: args.Timekeeper, c: sentrytime.Monotonic}
k.futexes = futex.NewManager()
k.netlinkPorts = port.New()
k.ptraceExceptions = make(map[*Task]*Task)
k.YAMAPtraceScope = linux.YAMA_SCOPE_RELATIONAL
if VFS2Enabled {
ctx := k.SupervisorContext()
@@ -425,7 +439,6 @@ func (k *Kernel) Init(args InitKernelArgs) error {
k.socketsVFS2 = make(map[*vfs.FileDescription]*SocketRecord)
}
return nil
}
+161 -14
View File
@@ -16,6 +16,7 @@ package kernel
import (
"fmt"
"sync/atomic"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/marshal/primitive"
@@ -95,7 +96,11 @@ const (
// checks for access mode PTRACE_MODE_ATTACH; otherwise, it checks for access
// mode PTRACE_MODE_READ.
//
// NOTE(b/30815691): The result of CanTrace is immediately stale (e.g., a
// In Linux, ptrace access restrictions may be configured by LSMs. While we do
// not support LSMs, we do add additional restrictions based on the commoncap
// and YAMA LSMs.
//
// TODO(gvisor.dev/issue/212): The result of CanTrace is immediately stale (e.g., a
// racing setuid(2) may change traceability). This may pose a risk when a task
// changes from traceable to not traceable. This is only problematic across
// execve, where privileges may increase.
@@ -103,7 +108,7 @@ const (
// We currently do not implement privileged executables (set-user/group-ID bits
// and file capabilities), so that case is not reachable.
func (t *Task) CanTrace(target *Task, attach bool) bool {
// "1. If the calling thread and the target thread are in the same thread
// "If the calling thread and the target thread are in the same thread
// group, access is always allowed." - ptrace(2)
//
// Note: Strictly speaking, prior to 73af963f9f30 ("__ptrace_may_access()
@@ -115,9 +120,57 @@ func (t *Task) CanTrace(target *Task, attach bool) bool {
return true
}
if !t.canTraceStandard(target, attach) {
return false
}
// YAMA only supported for vfs2.
if !VFS2Enabled {
return true
}
if atomic.LoadInt32(&t.k.YAMAPtraceScope) == linux.YAMA_SCOPE_RELATIONAL {
t.tg.pidns.owner.mu.RLock()
defer t.tg.pidns.owner.mu.RUnlock()
if !t.canTraceYAMALocked(target) {
return false
}
}
return true
}
// canTraceLocked is the same as CanTrace, except the caller must already hold
// the TaskSet mutex (for reading or writing).
func (t *Task) canTraceLocked(target *Task, attach bool) bool {
if t.tg == target.tg {
return true
}
if !t.canTraceStandard(target, attach) {
return false
}
// YAMA only supported for vfs2.
if !VFS2Enabled {
return true
}
if atomic.LoadInt32(&t.k.YAMAPtraceScope) == linux.YAMA_SCOPE_RELATIONAL {
if !t.canTraceYAMALocked(target) {
return false
}
}
return true
}
// canTraceStandard performs standard ptrace access checks as defined by
// kernel/ptrace.c:__ptrace_may_access as well as the commoncap LSM
// implementation of the security_ptrace_access_check() interface, which is
// always invoked.
func (t *Task) canTraceStandard(target *Task, attach bool) bool {
// """
// 2. If the access mode specifies PTRACE_MODE_FSCREDS (ED: snipped,
// doesn't exist until Linux 4.5).
// TODO(gvisor.dev/issue/260): 1. If the access mode specifies
// PTRACE_MODE_FSCREDS (ED: snipped, doesn't exist until Linux 4.5).
//
// Otherwise, the access mode specifies PTRACE_MODE_REALCREDS, so use the
// caller's real UID and GID for the checks in the next step. (Most APIs
@@ -125,7 +178,7 @@ func (t *Task) CanTrace(target *Task, attach bool) bool {
// historical reasons, the PTRACE_MODE_REALCREDS check uses the real IDs
// instead.)
//
// 3. Deny access if neither of the following is true:
// 2. Deny access if neither of the following is true:
//
// - The real, effective, and saved-set user IDs of the target match the
// caller's user ID, *and* the real, effective, and saved-set group IDs of
@@ -134,15 +187,12 @@ func (t *Task) CanTrace(target *Task, attach bool) bool {
// - The caller has the CAP_SYS_PTRACE capability in the user namespace of
// the target.
//
// 4. Deny access if the target process "dumpable" attribute has a value
// 3. Deny access if the target process "dumpable" attribute has a value
// other than 1 (SUID_DUMP_USER; see the discussion of PR_SET_DUMPABLE in
// prctl(2)), and the caller does not have the CAP_SYS_PTRACE capability in
// the user namespace of the target process.
//
// 5. The kernel LSM security_ptrace_access_check() interface is invoked to
// see if ptrace access is permitted. The results depend on the LSM(s). The
// implementation of this interface in the commoncap LSM performs the
// following steps:
// 4. The commoncap LSM performs the following steps:
//
// a) If the access mode includes PTRACE_MODE_FSCREDS, then use the
// caller's effective capability set; otherwise (the access mode specifies
@@ -188,6 +238,94 @@ func (t *Task) CanTrace(target *Task, attach bool) bool {
return true
}
// canTraceYAMALocked performs ptrace access checks as defined by the YAMA LSM
// implementation of the security_ptrace_access_check() interface, with YAMA
// configured to mode 1. This is a common default among various Linux
// distributions.
//
// It only permits the tracer to proceed if one of the following conditions is
// met:
//
// a) The tracer is already attached to the tracee.
//
// b) The target is a descendant of the tracer.
//
// c) The target has explicitly given permission to the tracer through the
// PR_SET_PTRACER prctl.
//
// d) The tracer has CAP_SYS_PTRACE.
//
// See security/yama/yama_lsm.c:yama_ptrace_access_check.
//
// Precondition: the TaskSet mutex must be locked (for reading or writing).
func (t *Task) canTraceYAMALocked(target *Task) bool {
if tracer := target.Tracer(); tracer != nil {
if tracer.tg == t.tg {
return true
}
}
if target.isYAMADescendantOfLocked(t) {
return true
}
if target.hasYAMAExceptionForLocked(t) {
return true
}
if t.HasCapabilityIn(linux.CAP_SYS_PTRACE, target.UserNamespace()) {
return true
}
return false
}
// Determines whether t is considered a descendant of ancestor for the purposes
// of YAMA permissions (specifically, whether t's thread group is descended from
// ancestor's).
//
// Precondition: the TaskSet mutex must be locked (for reading or writing).
func (t *Task) isYAMADescendantOfLocked(ancestor *Task) bool {
walker := t
for walker != nil {
if walker.tg.leader == ancestor.tg.leader {
return true
}
walker = walker.parent
}
return false
}
// Precondition: the TaskSet mutex must be locked (for reading or writing).
func (t *Task) hasYAMAExceptionForLocked(tracer *Task) bool {
allowed, ok := t.k.ptraceExceptions[t]
if !ok {
return false
}
return allowed == nil || tracer.isYAMADescendantOfLocked(allowed)
}
// ClearYAMAException removes any YAMA exception with t as the tracee.
func (t *Task) ClearYAMAException() {
t.tg.pidns.owner.mu.Lock()
defer t.tg.pidns.owner.mu.Unlock()
tracee := t.tg.leader
delete(t.k.ptraceExceptions, tracee)
}
// SetYAMAException creates a YAMA exception allowing all descendants of tracer
// to trace t. If tracer is nil, then any task is allowed to trace t.
//
// If there was an existing exception, it is overwritten with the new one.
func (t *Task) SetYAMAException(tracer *Task) {
t.tg.pidns.owner.mu.Lock()
defer t.tg.pidns.owner.mu.Unlock()
tracee := t.tg.leader
tracee.ptraceYAMAExceptionAdded = true
if tracer != nil {
tracer.ptraceYAMAExceptionAdded = true
}
t.k.ptraceExceptions[tracee] = tracer
}
// Tracer returns t's ptrace Tracer.
func (t *Task) Tracer() *Task {
return t.ptraceTracer.Load().(*Task)
@@ -358,7 +496,7 @@ func (t *Task) ptraceTraceme() error {
// returning nil here is correct.
return nil
}
if !t.parent.CanTrace(t, true) {
if !t.parent.canTraceLocked(t, true) {
return syserror.EPERM
}
if t.parent.exitState != TaskExitNone {
@@ -377,11 +515,11 @@ func (t *Task) ptraceAttach(target *Task, seize bool, opts uintptr) error {
if t.tg == target.tg {
return syserror.EPERM
}
if !t.CanTrace(target, true) {
return syserror.EPERM
}
t.tg.pidns.owner.mu.Lock()
defer t.tg.pidns.owner.mu.Unlock()
if !t.canTraceLocked(target, true) {
return syserror.EPERM
}
if target.hasTracer() {
return syserror.EPERM
}
@@ -459,6 +597,15 @@ func (t *Task) exitPtrace() {
}
// "nil maps cannot be saved"
t.ptraceTracees = make(map[*Task]struct{})
if t.ptraceYAMAExceptionAdded {
delete(t.k.ptraceExceptions, t)
for tracee, tracer := range t.k.ptraceExceptions {
if tracer == t {
delete(t.k.ptraceExceptions, tracee)
}
}
}
}
// forgetTracerLocked detaches t's tracer and ensures that t is no longer
+7
View File
@@ -403,6 +403,13 @@ type Task struct {
// ptraceEventMsg is protected by the TaskSet mutex.
ptraceEventMsg uint64
// ptraceYAMAExceptionAdded is true if a YAMA exception involving the task has
// been added before. This is used during task exit to decide whether we need
// to clean up YAMA exceptions.
//
// ptraceYAMAExceptionAdded is protected by the TaskSet mutex.
ptraceYAMAExceptionAdded bool
// The struct that holds the IO-related usage. The ioUsage pointer is
// immutable.
ioUsage *usage.IO
+2 -1
View File
@@ -694,7 +694,8 @@ func (t *Task) exitNotifyLocked(fromPtraceDetach bool) {
}
if t.parent != nil {
delete(t.parent.children, t)
t.parent = nil
// Do not clear t.parent. It may be still be needed after the task has exited
// (for example, to perform ptrace access checks on /proc/[pid] files).
}
}
}
+1 -1
View File
@@ -55,7 +55,7 @@ const InitTID ThreadID = 1
//
// +stateify savable
type TaskSet struct {
// mu protects all relationships betweens tasks and thread groups in the
// mu protects all relationships between tasks and thread groups in the
// TaskSet. (mu is approximately equivalent to Linux's tasklist_lock.)
mu sync.RWMutex `state:"nosave"`
+18
View File
@@ -172,6 +172,24 @@ func Prctl(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscall
}
return 1, nil, nil
case linux.PR_SET_PTRACER:
pid := args[1].Int()
switch pid {
case 0:
t.ClearYAMAException()
return 0, nil, nil
case linux.PR_SET_PTRACER_ANY:
t.SetYAMAException(nil)
return 0, nil, nil
default:
tracer := t.PIDNamespace().TaskWithID(kernel.ThreadID(pid))
if tracer == nil {
return 0, nil, syserror.EINVAL
}
t.SetYAMAException(tracer)
return 0, nil, nil
}
case linux.PR_SET_SECCOMP:
if args[1].Int() != linux.SECCOMP_MODE_FILTER {
// Unsupported mode.
+1
View File
@@ -1814,6 +1814,7 @@ cc_binary(
"@com_google_absl//absl/flags:flag",
"@com_google_absl//absl/time",
gtest,
"//test/util:capability_util",
"//test/util:file_descriptor",
"//test/util:fs_util",
"//test/util:logging",
File diff suppressed because it is too large Load Diff