Skip to content

Commit edc0a52

Browse files
committed
merge #55 into cyphar/filepath-securejoin:main
Aleksa Sarai (3): proc: add ProcPid helper proc: a few more lookup tests proc: remove safety warning comment LGTMs: cyphar
2 parents 5e78ca5 + 2a403a3 commit edc0a52

File tree

3 files changed

+178
-11
lines changed

3 files changed

+178
-11
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 & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,6 @@ var errUnsafeProcfs = errors.New("unsafe procfs detected")
216216
// [os.File]: https://pkg.go.dev/os#File
217217
type ProcThreadSelfCloser func()
218218

219-
// NOTE: THIS IS NOT YET SAFE TO EXPORT. The non-openat2(2) case is just using
220-
// a plain openat(2), which is not entirely safe against overmount attacks.
221-
// Yes, if we are using fsopen(2) or open_tree(2) (without AT_RECURSIVE), then
222-
// this is safe, but we shouldn't make less privileged users (or users on older
223-
// kernels) incorrectly assume this is safe. libpathrs does it correctly, and
224-
// it's best to leave it to them.
225219
func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ ProcThreadSelfCloser, Err error) {
226220
// If called from the external API, procRoot will be nil, so just get the
227221
// global root handle. It's also possible one of our tests calls this with
@@ -285,6 +279,34 @@ func ProcThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) {
285279
return procThreadSelf(nil, subpath)
286280
}
287281

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+
288310
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/[email protected], but in order to
289311
// avoid bumping the requirement for a single constant we can just define it
290312
// ourselves.

procfs_linux_test.go

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,16 +252,146 @@ 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.
258265
func TestProcThreadSelf(t *testing.T) {
259266
withWithoutOpenat2(t, true, func(t *testing.T) {
260-
handle, closer, err := ProcThreadSelf("stat")
261-
require.NoError(t, err, "ProcThreadSelf(stat)")
262-
require.NotNil(t, handle, "ProcThreadSelf(stat)")
263-
defer closer()
264-
defer handle.Close() //nolint:errcheck // test code
267+
t.Run("stat", func(t *testing.T) {
268+
handle, closer, err := ProcThreadSelf("stat")
269+
require.NoError(t, err, "ProcThreadSelf(stat)")
270+
require.NotNil(t, handle, "ProcThreadSelf(stat) handle")
271+
require.NotNil(t, closer, "ProcThreadSelf(stat) closer")
272+
defer closer()
273+
defer handle.Close() //nolint:errcheck // test code
274+
275+
realPath, err := ProcSelfFdReadlink(handle)
276+
require.NoError(t, err)
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+
}
282+
assert.Equal(t, wantPath, realPath, "final handle path")
283+
})
284+
285+
t.Run("abspath", func(t *testing.T) {
286+
handle, closer, err := ProcThreadSelf("/stat")
287+
require.NoError(t, err, "ProcThreadSelf(/stat)")
288+
require.NotNil(t, handle, "ProcThreadSelf(/stat) handle")
289+
require.NotNil(t, closer, "ProcThreadSelf(/stat) closer")
290+
defer closer()
291+
defer handle.Close() //nolint:errcheck // test code
292+
293+
realPath, err := ProcSelfFdReadlink(handle)
294+
require.NoError(t, err)
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+
}
300+
assert.Equal(t, wantPath, realPath, "final handle path")
301+
})
302+
303+
t.Run("wacky-abspath", func(t *testing.T) {
304+
handle, closer, err := ProcThreadSelf("////./////stat")
305+
require.NoError(t, err, "ProcThreadSelf(////./////stat)")
306+
require.NotNil(t, handle, "ProcThreadSelf(////./////stat) handle")
307+
require.NotNil(t, closer, "ProcThreadSelf(////./////stat) closer")
308+
defer closer()
309+
defer handle.Close() //nolint:errcheck // test code
310+
311+
realPath, err := ProcSelfFdReadlink(handle)
312+
require.NoError(t, err)
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+
}
318+
assert.Equal(t, wantPath, realPath, "final handle path")
319+
})
320+
321+
t.Run("dotdot", func(t *testing.T) {
322+
handle, closer, err := ProcThreadSelf("../../../../../../../../..")
323+
require.Error(t, err, "ProcThreadSelf(../...)")
324+
require.Nil(t, handle, "ProcThreadSelf(../...) handle")
325+
require.Nil(t, closer, "ProcThreadSelf(../...) closer")
326+
})
327+
328+
t.Run("wacky-dotdot", func(t *testing.T) {
329+
handle, closer, err := ProcThreadSelf("/../../../../../../../../..")
330+
require.Error(t, err, "ProcThreadSelf(/../...)")
331+
require.Nil(t, handle, "ProcThreadSelf(/../...) handle")
332+
require.Nil(t, closer, "ProcThreadSelf(/../...) closer")
333+
})
334+
})
335+
}
336+
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+
})
265395
})
266396
}
267397

0 commit comments

Comments
 (0)