Skip to content

Commit 2a403a3

Browse files
committed
proc: add ProcPid helper
This is basically a more minimal version of libpathrs's ProcRoot support, but limited to pids (our internal proc handle has subset=pids). libpathrs has more complicated handling to support for more cases, but those aren't really needed in filepath-securejoin. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent b2c3fde commit 2a403a3

File tree

3 files changed

+126
-3
lines changed

3 files changed

+126
-3
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2323
magiclinks) using this API. At the moment `filepath-securejoin` does not
2424
support this feature (but [libpathrs][] does).
2525

26+
* `ProcPid` is very similar to `ProcThreadSelf`, except it lets you get
27+
handles to subpaths of other processes. Unlike `ProcThreadSelf`, it is not
28+
necessary to lock the goroutine to the current thread and so no
29+
`ProcThreadSelfCloser` closure will be returned.
30+
31+
Please note that it is possible for you to be unable to access processes
32+
in certain configurations (when using `fsopen(2)`, the internal procfs
33+
mount will have `subset=pids,hidepids=traceable` mount options applied,
34+
which will hide many other processes and any non-process-related top-level
35+
files), but `ProcPid(os.Getpid(), ...)` (to access the current
36+
thread-group leader) should always work.
37+
38+
Also note that if the target process dies, the handle you received from
39+
`ProcPid` may start returning errors or blank data when you operate on it.
40+
2641
* `ProcSelfFdReadlink` lets you get the in-kernel path representation of a
2742
file descriptor (think `readlink("/proc/self/fd/...")`). This is
2843
equivalent to doing a `readlinkat(fd, "", ...)` of

procfs_linux.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,34 @@ func ProcThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) {
279279
return procThreadSelf(nil, subpath)
280280
}
281281

282+
// ProcPid returns a handle to /proc/$pid/<subpath> (pid can be a pid or tid).
283+
// You should not use this for the current thread, as special handling is
284+
// needed for /proc/thread-self (or /proc/self/task/<tid>) when dealing with
285+
// goroutine scheduling -- use [ProcThreadSelf] instead. This is mainly
286+
// intended for usage when operating on other processes.
287+
//
288+
// You should not try to operate on the top-level /proc handle (such as by
289+
// setting subpath to "../foo"). This will not work at all on non-openat2
290+
// systems, and when using an internal fsopen-based handle, the mount will have
291+
// subset=pids and hidepid=traceable set (which will restrict what PIDs can be
292+
// accessed with this API, as well as removing any non-PID procfs files).
293+
func ProcPid(pid int, subpath string) (*os.File, error) {
294+
procRoot, err := getProcRoot()
295+
if err != nil {
296+
return nil, err
297+
}
298+
299+
handle, err := procfsLookupInRoot(procRoot, strconv.Itoa(pid)+"/"+subpath)
300+
if err != nil {
301+
// TODO: Once we bump the minimum Go version to 1.20, we can use
302+
// multiple %w verbs for this wrapping. For now we need to use a
303+
// compatibility shim for older Go versions.
304+
// err = fmt.Errorf("%w: %w", errUnsafeProcfs, err)
305+
return nil, wrapBaseError(err, errUnsafeProcfs)
306+
}
307+
return handle, nil
308+
}
309+
282310
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/[email protected], but in order to
283311
// avoid bumping the requirement for a single constant we can just define it
284312
// ourselves.

procfs_linux_test.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,13 @@ func TestProcOvermountSubdir_ProcThreadSelf(t *testing.T) {
252252
})
253253
}
254254

255+
// isFsopenRoot returns whether the internal procfs handle is an fsopen root.
256+
func isFsopenRoot(t *testing.T) bool {
257+
procRoot, err := getProcRoot()
258+
require.NoError(t, err)
259+
return procRoot.Name() == "fsmount:fscontext:proc"
260+
}
261+
255262
// Because of the introduction of protections against /proc overmounts,
256263
// ProcThreadSelf will not be called in actual tests unless we have a basic
257264
// test here.
@@ -267,7 +274,11 @@ func TestProcThreadSelf(t *testing.T) {
267274

268275
realPath, err := ProcSelfFdReadlink(handle)
269276
require.NoError(t, err)
270-
wantPath := fmt.Sprintf("/proc/%d/task/%d/stat", os.Getpid(), unix.Gettid())
277+
wantPath := fmt.Sprintf("/%d/task/%d/stat", os.Getpid(), unix.Gettid())
278+
if !isFsopenRoot(t) {
279+
// The /proc prefix is only present when not using fsopen.
280+
wantPath = "/proc" + wantPath
281+
}
271282
assert.Equal(t, wantPath, realPath, "final handle path")
272283
})
273284

@@ -281,7 +292,11 @@ func TestProcThreadSelf(t *testing.T) {
281292

282293
realPath, err := ProcSelfFdReadlink(handle)
283294
require.NoError(t, err)
284-
wantPath := fmt.Sprintf("/proc/%d/task/%d/stat", os.Getpid(), unix.Gettid())
295+
wantPath := fmt.Sprintf("/%d/task/%d/stat", os.Getpid(), unix.Gettid())
296+
if !isFsopenRoot(t) {
297+
// The /proc prefix is only present when not using fsopen.
298+
wantPath = "/proc" + wantPath
299+
}
285300
assert.Equal(t, wantPath, realPath, "final handle path")
286301
})
287302

@@ -295,7 +310,11 @@ func TestProcThreadSelf(t *testing.T) {
295310

296311
realPath, err := ProcSelfFdReadlink(handle)
297312
require.NoError(t, err)
298-
wantPath := fmt.Sprintf("/proc/%d/task/%d/stat", os.Getpid(), unix.Gettid())
313+
wantPath := fmt.Sprintf("/%d/task/%d/stat", os.Getpid(), unix.Gettid())
314+
if !isFsopenRoot(t) {
315+
// The /proc prefix is only present when not using fsopen.
316+
wantPath = "/proc" + wantPath
317+
}
299318
assert.Equal(t, wantPath, realPath, "final handle path")
300319
})
301320

@@ -315,6 +334,67 @@ func TestProcThreadSelf(t *testing.T) {
315334
})
316335
}
317336

337+
func TestProcPid(t *testing.T) {
338+
withWithoutOpenat2(t, true, func(t *testing.T) {
339+
t.Run("pid1-stat", func(t *testing.T) {
340+
handle, err := ProcPid(1, "stat")
341+
require.NoError(t, err, "ProcPid(1, stat)")
342+
require.NotNil(t, handle, "ProcPid(1, stat) handle")
343+
344+
realPath, err := ProcSelfFdReadlink(handle)
345+
require.NoError(t, err)
346+
wantPath := "/1/stat"
347+
if !isFsopenRoot(t) {
348+
// The /proc prefix is only present when not using fsopen.
349+
wantPath = "/proc" + wantPath
350+
}
351+
assert.Equal(t, wantPath, realPath, "final handle path")
352+
})
353+
354+
t.Run("pid1-stat-abspath", func(t *testing.T) {
355+
handle, err := ProcPid(1, "/stat")
356+
require.NoError(t, err, "ProcPid(1, /stat)")
357+
require.NotNil(t, handle, "ProcPid(1, /stat) handle")
358+
359+
realPath, err := ProcSelfFdReadlink(handle)
360+
require.NoError(t, err)
361+
wantPath := "/1/stat"
362+
if !isFsopenRoot(t) {
363+
// The /proc prefix is only present when not using fsopen.
364+
wantPath = "/proc" + wantPath
365+
}
366+
assert.Equal(t, wantPath, realPath, "final handle path")
367+
})
368+
369+
t.Run("pid1-stat-wacky-abspath", func(t *testing.T) {
370+
handle, err := ProcPid(1, "////.////stat")
371+
require.NoError(t, err, "ProcPid(1, ////.////stat)")
372+
require.NotNil(t, handle, "ProcPid(1, ////.////stat) handle")
373+
374+
realPath, err := ProcSelfFdReadlink(handle)
375+
require.NoError(t, err)
376+
wantPath := "/1/stat"
377+
if !isFsopenRoot(t) {
378+
// The /proc prefix is only present when not using fsopen.
379+
wantPath = "/proc" + wantPath
380+
}
381+
assert.Equal(t, wantPath, realPath, "final handle path")
382+
})
383+
384+
t.Run("dotdot", func(t *testing.T) {
385+
handle, err := ProcPid(1, "../../../../../../../../..")
386+
require.Error(t, err, "ProcPid(1, ../...)")
387+
require.Nil(t, handle, "ProcPid(1, ../...) handle")
388+
})
389+
390+
t.Run("wacky-dotdot", func(t *testing.T) {
391+
handle, err := ProcPid(1, "/../../../../../../../../..")
392+
require.Error(t, err, "ProcPid(1, /../...)")
393+
require.Nil(t, handle, "ProcPid(1, /../...) handle")
394+
})
395+
})
396+
}
397+
318398
func canFsOpen() bool {
319399
f, err := fsopen("tmpfs", 0)
320400
if f != nil {

0 commit comments

Comments
 (0)