Skip to content

Commit 894399a

Browse files
committed
proc: export ProcThreadSelf and ProcSelfFdReadlink
These are quite useful for protecting against a variety of attacks, and mirror the general API from libpathrs but a little bit less fully-featured. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent c41bcd7 commit 894399a

File tree

8 files changed

+133
-32
lines changed

8 files changed

+133
-32
lines changed

CHANGELOG.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77
## [Unreleased] ##
88

99
### Added ###
10+
- Some minimal parts of the safe `procfs` API have now been exposed. At the
11+
moment these include:
12+
13+
* `ProcThreadSelf` which allows you to get a safe handle `O_PATH` to a
14+
subpath in `/proc/thread-self` (you can upgrade it to a proper handle with
15+
`Reopen` like any other `O_PATH` handle). The returned
16+
`ProcThreadSelfCloser` needs to be called after you completely finish
17+
using the handle (this is necessary because Go is multi-threaded and
18+
`ProcThreadSelf` references `/proc/thread-self` which may disappear if we
19+
do not `runtime.LockOSThread` -- `ProcThreadSelfCloser` is currently
20+
equivalent to `runtime.UnlockOSThread`).
21+
22+
Note that you cannot operate on any `procfs` symlinks (most notably
23+
magiclinks) using this API. At the moment `filepath-securejoin` does not
24+
support this feature (but [libpathrs][] does).
25+
26+
* `ProcSelfFdReadlink` lets you get the in-kernel path representation of a
27+
file descriptor (think `readlink("/proc/self/fd/...")`). This is
28+
equivalent to doing a `readlinkat(fd, "", ...)` of
29+
`ProcThreadSelf("fd/%d")`, except that we verify that there aren't any
30+
tricky overmounts that could fool the process.
31+
32+
Please be aware that the returned string is simply a snapshot at that
33+
particular moment, and an attacker could move the file being pointed to.
34+
In addition, complex namespace configurations could result in non-sensical
35+
or confusing paths to be returned. The value received from this function
36+
should only be used as secondary verification of some security property,
37+
not as proof that a particular handle has a particular path.
38+
39+
The procfs handle used internally by the API is the same as the rest of
40+
`filepath-securejoin` (for privileged programs this is usually a private
41+
in-process `procfs` instance created with `fsopen(2)`).
42+
43+
As before, this is intended as a stop-gap before users migrate to
44+
[libpathrs][], which provides a far more extensive safe `procfs` API and is
45+
generally more robust.
46+
1047
- Previously, the hardened procfs implementation (used internally within
1148
`Reopen` and `Open(at)InRoot`) only protected against overmount attacks on
1249
systems with `openat2(2)` (Linux 5.6) or systems with `fsopen(2)` or
@@ -20,7 +57,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2057
This was considered a reasonable trade-off, as the long-term intention was to
2158
get all users to just switch to [libpathrs][] if they wanted to use the safe
2259
`procfs` API (which had more extensive protections, and is what these new
23-
protections in `filepath-securejoin` are based on).
60+
protections in `filepath-securejoin` are based on). However, as the API
61+
is now being exported it seems unwise to advertise the API as "safe" if we do
62+
not protect against known attacks.
2463

2564
The procfs API will now be more protected against attackers on systems
2665
lacking the aforementioned protections. However, the most comprehensive of

lookup_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.Fi
190190
// root is some magic-link like /proc/$pid/root, in which case we want to
191191
// make sure when we do checkProcSelfFdPath that we are using the correct
192192
// root path.
193-
logicalRootPath, err := procSelfFdReadlink(root)
193+
logicalRootPath, err := ProcSelfFdReadlink(root)
194194
if err != nil {
195195
return nil, "", fmt.Errorf("get real root path: %w", err)
196196
}

lookup_linux_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func checkPartialLookup(t *testing.T, partialLookupFn partialLookupFunc, rootDir
5252
assert.Equal(t, expected.remainingPath, remainingPath, "remaining path")
5353

5454
// Check the handle path.
55-
gotPath, err := procSelfFdReadlink(handle)
55+
gotPath, err := ProcSelfFdReadlink(handle)
5656
require.NoError(t, err, "get real path of returned handle")
5757
assert.Equal(t, expected.handlePath, gotPath, "real handle path")
5858
// Make sure the handle matches the readlink path.
@@ -371,9 +371,9 @@ func (m *racingLookupMeta) checkPartialLookup(t *testing.T, rootDir *os.File, un
371371
if handle != nil {
372372
handleName = handle.Name()
373373

374-
// Get the "proper" name from procSelfFdReadlink.
374+
// Get the "proper" name from ProcSelfFdReadlink.
375375
m.pauseCh <- struct{}{}
376-
realPath, err = procSelfFdReadlink(handle)
376+
realPath, err = ProcSelfFdReadlink(handle)
377377
<-m.pauseCh
378378
require.NoError(t, err, "get real path of returned handle")
379379

mkdir_linux_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ var mkdirAll_MkdirAllHandle mkdirAllFunc = func(t *testing.T, root, unsafePath s
4646
require.NoError(t, err)
4747

4848
// Now double-check that the handle is correct.
49-
gotPath, err := procSelfFdReadlink(handle)
49+
gotPath, err := ProcSelfFdReadlink(handle)
5050
require.NoError(t, err, "get real path of returned handle")
5151
assert.Equal(t, expectedPath, gotPath, "wrong final path from MkdirAllHandle")
5252
// Also check that the f.Name() is correct while we're at it (this is

open_linux_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ func checkReopen(t *testing.T, handle *os.File, flags int, expectedErr error) {
6262
require.NoError(t, err)
6363

6464
// Get the original handle path.
65-
handlePath, err := procSelfFdReadlink(handle)
65+
handlePath, err := ProcSelfFdReadlink(handle)
6666
require.NoError(t, err, "get real path of original handle")
6767
// Make sure the handle matches the readlink path.
6868
assert.Equal(t, handlePath, handle.Name(), "handle.Name() matching real original handle path")
6969

7070
// Check that the new and old handle have the same path.
71-
newHandlePath, err := procSelfFdReadlink(newHandle)
71+
newHandlePath, err := ProcSelfFdReadlink(newHandle)
7272
require.NoError(t, err, "get real path of reopened handle")
7373
assert.Equal(t, handlePath, newHandlePath, "old and reopen handle paths")
7474
assert.Equal(t, handle.Name(), newHandle.Name(), "old and reopen handle.Name()")
@@ -102,7 +102,7 @@ func checkOpenInRoot(t *testing.T, openInRootFn openInRootFunc, root, unsafePath
102102
require.NoError(t, err)
103103

104104
// Check the handle path.
105-
gotPath, err := procSelfFdReadlink(handle)
105+
gotPath, err := ProcSelfFdReadlink(handle)
106106
require.NoError(t, err, "get real path of returned handle")
107107
assert.Equal(t, expected.handlePath, gotPath, "real handle path")
108108
// Make sure the handle matches the readlink path.

procfs_linux.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -210,24 +210,30 @@ var hasProcThreadSelf = sync_OnceValue(func() bool {
210210

211211
var errUnsafeProcfs = errors.New("unsafe procfs detected")
212212

213-
type procThreadSelfCloser func()
214-
215-
// procThreadSelf returns a handle to /proc/thread-self/<subpath> (or an
216-
// equivalent handle on older kernels where /proc/thread-self doesn't exist).
217-
// Once finished with the handle, you must call the returned closer function
218-
// (runtime.UnlockOSThread). You must not pass the returned *os.File to other
219-
// Go threads or use the handle after calling the closer.
220-
//
221-
// This is similar to ProcThreadSelf from runc, but with extra hardening
222-
// applied and using *os.File.
213+
// ProcThreadSelfCloser is a callback that needs to be called when you are done
214+
// operating on an [os.File] fetched using [ProcThreadSelf].
223215
//
216+
// [os.File]: https://pkg.go.dev/os#File
217+
type ProcThreadSelfCloser func()
218+
224219
// NOTE: THIS IS NOT YET SAFE TO EXPORT. The non-openat2(2) case is just using
225220
// a plain openat(2), which is not entirely safe against overmount attacks.
226221
// Yes, if we are using fsopen(2) or open_tree(2) (without AT_RECURSIVE), then
227222
// this is safe, but we shouldn't make less privileged users (or users on older
228223
// kernels) incorrectly assume this is safe. libpathrs does it correctly, and
229224
// it's best to leave it to them.
230-
func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ procThreadSelfCloser, Err error) {
225+
func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ ProcThreadSelfCloser, Err error) {
226+
// If called from the external API, procRoot will be nil, so just get the
227+
// global root handle. It's also possible one of our tests calls this with
228+
// nil by accident, so we should handle the case anyway.
229+
if procRoot == nil {
230+
root, err := getProcRoot()
231+
if err != nil {
232+
return nil, nil, err
233+
}
234+
procRoot = root
235+
}
236+
231237
// We need to lock our thread until the caller is done with the handle
232238
// because between getting the handle and using it we could get interrupted
233239
// by the Go runtime and hit the case where the underlying thread is
@@ -267,6 +273,18 @@ func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ procThread
267273
return handle, runtime.UnlockOSThread, nil
268274
}
269275

276+
// ProcThreadSelf returns a handle to /proc/thread-self/<subpath> (or an
277+
// equivalent handle on older kernels where /proc/thread-self doesn't exist).
278+
// Once finished with the handle, you must call the returned closer function
279+
// (runtime.UnlockOSThread). You must not pass the returned *os.File to other
280+
// Go threads or use the handle after calling the closer.
281+
//
282+
// This is similar to ProcThreadSelf from runc, but with extra hardening
283+
// applied and using *os.File.
284+
func ProcThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) {
285+
return procThreadSelf(nil, subpath)
286+
}
287+
270288
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/[email protected], but in order to
271289
// avoid bumping the requirement for a single constant we can just define it
272290
// ourselves.
@@ -372,7 +390,10 @@ func rawProcSelfFdReadlink(fd int) (string, error) {
372390
return doRawProcSelfFdReadlink(procRoot, fd)
373391
}
374392

375-
func procSelfFdReadlink(f *os.File) (string, error) {
393+
// ProcSelfFdReadlink gets the real path of the given file by looking at
394+
// readlink(/proc/thread-self/fd/$n), with the same safety protections as
395+
// [ProcThreadSelf] (as well as some additional checks against overmounts).
396+
func ProcSelfFdReadlink(f *os.File) (string, error) {
376397
return rawProcSelfFdReadlink(int(f.Fd()))
377398
}
378399

@@ -404,7 +425,7 @@ func checkProcSelfFdPath(path string, file *os.File) error {
404425
if err := isDeadInode(file); err != nil {
405426
return err
406427
}
407-
actualPath, err := procSelfFdReadlink(file)
428+
actualPath, err := ProcSelfFdReadlink(file)
408429
if err != nil {
409430
return fmt.Errorf("get path of handle: %w", err)
410431
}

procfs_linux_test.go

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,15 @@ func setupMountNamespace(t *testing.T) {
7777
require.NoError(t, err)
7878
}
7979

80+
func doProcThreadSelf(procRoot *os.File, subpath string) (*os.File, ProcThreadSelfCloser, error) {
81+
if procRoot != nil {
82+
return procThreadSelf(procRoot, subpath)
83+
}
84+
return ProcThreadSelf(subpath)
85+
}
86+
8087
func testProcThreadSelf(t *testing.T, procRoot *os.File, subpath string, expectErr bool) {
81-
handle, closer, err := procThreadSelf(procRoot, subpath)
88+
handle, closer, err := doProcThreadSelf(procRoot, subpath)
8289
if expectErr {
8390
assert.ErrorIsf(t, err, errUnsafeProcfs, "should have detected /proc/thread-self/%s overmount", subpath)
8491
} else if assert.NoErrorf(t, err, "/proc/thread-self/%s open should succeed", subpath) {
@@ -127,7 +134,9 @@ func testProcOvermountSubdir(t *testing.T, procRootFn procRootFunc, expectOvermo
127134

128135
procRoot, err := procRootFn()
129136
require.NoError(t, err)
130-
defer procRoot.Close() //nolint:errcheck // test code
137+
if procRoot != nil {
138+
defer procRoot.Close() //nolint:errcheck // test code
139+
}
131140

132141
// For both tmpfs and procfs overmounts, we should catch them (with or
133142
// without openat2, thanks to procfsLookupInRoot).
@@ -140,7 +149,7 @@ func testProcOvermountSubdir(t *testing.T, procRootFn procRootFunc, expectOvermo
140149
symlinkOvermountErr = nil
141150
}
142151

143-
procSelf, closer, err := procThreadSelf(procRoot, ".")
152+
procSelf, closer, err := doProcThreadSelf(procRoot, ".")
144153
require.NoError(t, err)
145154
defer procSelf.Close() //nolint:errcheck // test code
146155
defer closer()
@@ -155,22 +164,31 @@ func testProcOvermountSubdir(t *testing.T, procRootFn procRootFunc, expectOvermo
155164
require.NoError(t, err)
156165
defer procExe.Close() //nolint:errcheck // test code
157166

167+
// Get the handle that doProcThreadSelf is using internally when we
168+
// test the external API.
169+
testProcRoot := procRoot
170+
if testProcRoot == nil {
171+
testProcRoot, err = getProcRoot()
172+
require.NoError(t, err)
173+
// do not call testProcRoot.Close() -- it's a global handle
174+
}
175+
158176
// no overmount
159-
err = checkSubpathOvermount(procRoot, procCwd, "")
177+
err = checkSubpathOvermount(testProcRoot, procCwd, "")
160178
assert.NoError(t, err, "checking /proc/self/cwd with no overmount should succeed") //nolint:testifylint // this is an isolated operation so we can continue despite an error
161-
err = checkSubpathOvermount(procRoot, procSelf, "cwd")
179+
err = checkSubpathOvermount(testProcRoot, procSelf, "cwd")
162180
assert.NoError(t, err, "checking /proc/self/cwd with no overmount should succeed") //nolint:testifylint // this is an isolated operation so we can continue despite an error
163181
// basic overmount
164-
err = checkSubpathOvermount(procRoot, procExe, "")
182+
err = checkSubpathOvermount(testProcRoot, procExe, "")
165183
assert.ErrorIs(t, err, symlinkOvermountErr, "unexpected /proc/self/exe overmount result") //nolint:testifylint // this is an isolated operation so we can continue despite an error
166-
err = checkSubpathOvermount(procRoot, procSelf, "exe")
184+
err = checkSubpathOvermount(testProcRoot, procSelf, "exe")
167185
assert.ErrorIs(t, err, symlinkOvermountErr, "unexpected /proc/self/exe overmount result") //nolint:testifylint // this is an isolated operation so we can continue despite an error
168186

169187
// fd no overmount
170-
_, err = doRawProcSelfFdReadlink(procRoot, 1)
188+
_, err = doRawProcSelfFdReadlink(testProcRoot, 1)
171189
assert.NoError(t, err, "checking /proc/self/fd/1 with no overmount should succeed") //nolint:testifylint // this is an isolated operation so we can continue despite an error
172190
// fd overmount
173-
link, err := doRawProcSelfFdReadlink(procRoot, 0)
191+
link, err := doRawProcSelfFdReadlink(testProcRoot, 0)
174192
assert.ErrorIs(t, err, symlinkOvermountErr, "unexpected /proc/self/fd/0 overmount result: got link %q", link) //nolint:testifylint // this is an isolated operation so we can continue despite an error
175193
})
176194
}
@@ -224,6 +242,29 @@ func TestProcOvermountSubdir_doGetProcRoot_Mocked(t *testing.T) {
224242
})
225243
}
226244

245+
func TestProcOvermountSubdir_ProcThreadSelf(t *testing.T) {
246+
withWithoutOpenat2(t, true, func(t *testing.T) {
247+
testForceProcThreadSelf(t, func(t *testing.T) {
248+
dummyGetRoot := func() (*os.File, error) { return nil, nil } //nolint:nilnil // intentional
249+
// Use the same overmounts policy as doGetProcRoot.
250+
testProcOvermountSubdir(t, dummyGetRoot, !hasNewMountAPI())
251+
})
252+
})
253+
}
254+
255+
// Because of the introduction of protections against /proc overmounts,
256+
// ProcThreadSelf will not be called in actual tests unless we have a basic
257+
// test here.
258+
func TestProcThreadSelf(t *testing.T) {
259+
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
265+
})
266+
}
267+
227268
func canFsOpen() bool {
228269
f, err := fsopen("tmpfs", 0)
229270
if f != nil {

procfs_lookup_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func procfsLookupInRoot(procRoot *os.File, unsafePath string) (Handle *os.File,
6969
// nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it.
7070
//
7171
// NOTE: We MUST NOT use RESOLVE_IN_ROOT here, as openat2File uses
72-
// procSelfFdReadlink to clean up the returned f.Name() if we use
72+
// ProcSelfFdReadlink to clean up the returned f.Name() if we use
7373
// RESOLVE_IN_ROOT (which would lead to an infinite recursion).
7474
//
7575
// TODO: It would be nice to have RESOLVE_NO_DOTDOT, purely for

0 commit comments

Comments
 (0)