1# Changelog #
  2All notable changes to this project will be documented in this file.
  3
  4The format is based on [Keep a Changelog](http://keepachangelog.com/)
  5and this project adheres to [Semantic Versioning](http://semver.org/).
  6
  7## [Unreleased] ##
  8
  9## [0.6.1] - 2025-11-19 ##
 10
 11> At last up jumped the cunning spider, and fiercely held her fast.
 12
 13### Fixed ###
 14- Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH`
 15  resolver would cache the result to avoid doing needless test runs of
 16  `openat2(2)`. However, this causes issues when `pathrs-lite` is being used by
 17  a program that applies new seccomp-bpf filters onto itself -- if the filter
 18  denies `openat2(2)` then we would return that error rather than falling back
 19  to the `O_PATH` resolver. To resolve this issue, we no longer cache the
 20  result if `openat2(2)` was successful, only if there was an error.
 21- A file descriptor leak in our `openat2` wrapper (when doing the necessary
 22  `dup` for `RESOLVE_IN_ROOT`) has been removed.
 23
 24## [0.5.2] - 2025-11-19 ##
 25
 26> "Will you walk into my parlour?" said a spider to a fly.
 27
 28### Fixed ###
 29- Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH`
 30  resolver would cache the result to avoid doing needless test runs of
 31  `openat2(2)`. However, this causes issues when `pathrs-lite` is being used by
 32  a program that applies new seccomp-bpf filters onto itself -- if the filter
 33  denies `openat2(2)` then we would return that error rather than falling back
 34  to the `O_PATH` resolver. To resolve this issue, we no longer cache the
 35  result if `openat2(2)` was successful, only if there was an error.
 36- A file descriptor leak in our `openat2` wrapper (when doing the necessary
 37  `dup` for `RESOLVE_IN_ROOT`) has been removed.
 38
 39## [0.6.0] - 2025-11-03 ##
 40
 41> By the Power of Greyskull!
 42
 43### Breaking ###
 44- The deprecated `MkdirAll`, `MkdirAllHandle`, `OpenInRoot`, `OpenatInRoot` and
 45  `Reopen` wrappers have been removed. Please switch to using `pathrs-lite`
 46  directly.
 47
 48### Added ###
 49- `pathrs-lite` now has support for using libpathrs as a backend. This is
 50  opt-in and can be enabled at build time with the `libpathrs` build tag. The
 51  intention is to allow for downstream libraries and other projects to make use
 52  of the pure-Go `github.com/cyphar/filepath-securejoin/pathrs-lite` package
 53  and distributors can then opt-in to using `libpathrs` for the entire binary
 54  if they wish.
 55
 56## [0.5.1] - 2025-10-31 ##
 57
 58> Spooky scary skeletons send shivers down your spine!
 59
 60### Changed ###
 61- `openat2` can return `-EAGAIN` if it detects a possible attack in certain
 62  scenarios (namely if there was a rename or mount while walking a path with a
 63  `..` component). While this is necessary to avoid a denial-of-service in the
 64  kernel, it does require retry loops in userspace.
 65
 66  In previous versions, `pathrs-lite` would retry `openat2` 32 times before
 67  returning an error, but we've received user reports that this limit can be
 68  hit on systems with very heavy load. In some synthetic benchmarks (testing
 69  the worst-case of an attacker doing renames in a tight loop on every core of
 70  a 16-core machine) we managed to get a ~3% failure rate in runc. We have
 71  improved this situation in two ways:
 72
 73  * We have now increased this limit to 128, which should be good enough for
 74    most use-cases without becoming a denial-of-service vector (the number of
 75    syscalls called by the `O_PATH` resolver in a typical case is within the
 76    same ballpark). The same benchmarks show a failure rate of ~0.12% which
 77    (while not zero) is probably sufficient for most users.
 78
 79  * In addition, we now return a `unix.EAGAIN` error that is bubbled up and can
 80    be detected by callers. This means that callers with stricter requirements
 81    to avoid spurious errors can choose to do their own infinite `EAGAIN` retry
 82    loop (though we would strongly recommend users use time-based deadlines in
 83    such retry loops to avoid potentially unbounded denials-of-service).
 84
 85## [0.5.0] - 2025-09-26 ##
 86
 87> Let the past die. Kill it if you have to.
 88
 89> **NOTE**: With this release, some parts of
 90> `github.com/cyphar/filepath-securejoin` are now licensed under the Mozilla
 91> Public License (version 2). Please see [COPYING.md][] as well as the the
 92> license header in each file for more details.
 93
 94[COPYING.md]: ./COPYING.md
 95
 96### Breaking ###
 97- The new API introduced in the [0.3.0][] release has been moved to a new
 98  subpackage called `pathrs-lite`. This was primarily done to better indicate
 99  the split between the new and old APIs, as well as indicate to users the
100  purpose of this subpackage (it is a less complete version of [libpathrs][]).
101
102  We have added some wrappers to the top-level package to ease the transition,
103  but those are deprecated and will be removed in the next minor release of
104  filepath-securejoin. Users should update their import paths.
105
106  This new subpackage has also been relicensed under the Mozilla Public License
107  (version 2), please see [COPYING.md][] for more details.
108
109### Added ###
110- Most of the key bits the safe `procfs` API have now been exported and are
111  available in `github.com/cyphar/filepath-securejoin/pathrs-lite/procfs`. At
112  the moment this primarily consists of a new `procfs.Handle` API:
113
114   * `OpenProcRoot` returns a new handle to `/proc`, endeavouring to make it
115     safe if possible (`subset=pid` to protect against mistaken write attacks
116     and leaks, as well as using `fsopen(2)` to avoid racing mount attacks).
117
118     `OpenUnsafeProcRoot` returns a handle without attempting to create one
119     with `subset=pid`, which makes it more dangerous to leak. Most users
120     should use `OpenProcRoot` (even if you need to use `ProcRoot` as the base
121     of an operation, as filepath-securejoin will internally open a handle when
122     necessary).
123
124   * The `(*procfs.Handle).Open*` family of methods lets you get a safe
125     `O_PATH` handle to subpaths within `/proc` for certain subpaths.
126
127     For `OpenThreadSelf`, the returned `ProcThreadSelfCloser` needs to be
128     called after you completely finish using the handle (this is necessary
129     because Go is multi-threaded and `ProcThreadSelf` references
130     `/proc/thread-self` which may disappear if we do not
131     `runtime.LockOSThread` -- `ProcThreadSelfCloser` is currently equivalent
132     to `runtime.UnlockOSThread`).
133
134     Note that you cannot open any `procfs` symlinks (most notably magic-links)
135     using this API. At the moment, filepath-securejoin does not support this
136     feature (but [libpathrs][] does).
137
138   * `ProcSelfFdReadlink` lets you get the in-kernel path representation of a
139     file descriptor (think `readlink("/proc/self/fd/...")`), except that we
140     verify that there aren't any tricky overmounts that could fool the
141     process.
142
143     Please be aware that the returned string is simply a snapshot at that
144     particular moment, and an attacker could move the file being pointed to.
145     In addition, complex namespace configurations could result in non-sensical
146     or confusing paths to be returned. The value received from this function
147     should only be used as secondary verification of some security property,
148     not as proof that a particular handle has a particular path.
149
150  The procfs handle used internally by the API is the same as the rest of
151  `filepath-securejoin` (for privileged programs this is usually a private
152  in-process `procfs` instance created with `fsopen(2)`).
153
154  As before, this is intended as a stop-gap before users migrate to
155  [libpathrs][], which provides a far more extensive safe `procfs` API and is
156  generally more robust.
157
158- Previously, the hardened procfs implementation (used internally within
159  `Reopen` and `Open(at)InRoot`) only protected against overmount attacks on
160  systems with `openat2(2)` (Linux 5.6) or systems with `fsopen(2)` or
161  `open_tree(2)` (Linux 5.2) and programs with privileges to use them (with
162  some caveats about locked mounts that probably affect very few users). For
163  other users, an attacker with the ability to create malicious mounts (on most
164  systems, a sysadmin) could trick you into operating on files you didn't
165  expect. This attack only really makes sense in the context of container
166  runtime implementations.
167
168  This was considered a reasonable trade-off, as the long-term intention was to
169  get all users to just switch to [libpathrs][] if they wanted to use the safe
170  `procfs` API (which had more extensive protections, and is what these new
171  protections in `filepath-securejoin` are based on). However, as the API
172  is now being exported it seems unwise to advertise the API as "safe" if we do
173  not protect against known attacks.
174
175  The procfs API is now more protected against attackers on systems lacking the
176  aforementioned protections. However, the most comprehensive of these
177  protections effectively rely on [`statx(STATX_MNT_ID)`][statx.2] (Linux 5.8).
178  On older kernel versions, there is no effective protection (there is some
179  minimal protection against non-`procfs` filesystem components but a
180  sufficiently clever attacker can work around those). In addition,
181  `STATX_MNT_ID` is vulnerable to mount ID reuse attacks by sufficiently
182  motivated and privileged attackers -- this problem is mitigated with
183  `STATX_MNT_ID_UNIQUE` (Linux 6.8) but that raises the minimum kernel version
184  for more protection.
185
186  The fact that these protections are quite limited despite needing a fair bit
187  of extra code to handle was one of the primary reasons we did not initially
188  implement this in `filepath-securejoin` ([libpathrs][] supports all of this,
189  of course).
190
191### Fixed ###
192- RHEL 8 kernels have backports of `fsopen(2)` but in some testing we've found
193  that it has very bad (and very difficult to debug) performance issues, and so
194  we will explicitly refuse to use `fsopen(2)` if the running kernel version is
195  pre-5.2 and will instead fallback to `open("/proc")`.
196
197[CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
198[libpathrs]: https://github.com/cyphar/libpathrs
199[statx.2]: https://www.man7.org/linux/man-pages/man2/statx.2.html
200
201## [0.4.1] - 2025-01-28 ##
202
203### Fixed ###
204- The restrictions added for `root` paths passed to `SecureJoin` in 0.4.0 was
205  found to be too strict and caused some regressions when folks tried to
206  update, so this restriction has been relaxed to only return an error if the
207  path contains a `..` component. We still recommend users use `filepath.Clean`
208  (and even `filepath.EvalSymlinks`) on the `root` path they are using, but at
209  least you will no longer be punished for "trivial" unclean paths.
210
211## [0.4.0] - 2025-01-13 ##
212
213### Breaking ####
214- `SecureJoin(VFS)` will now return an error if the provided `root` is not a
215  `filepath.Clean`'d path.
216
217  While it is ultimately the responsibility of the caller to ensure the root is
218  a safe path to use, passing a path like `/symlink/..` as a root would result
219  in the `SecureJoin`'d path being placed in `/` even though `/symlink/..`
220  might be a different directory, and so we should more strongly discourage
221  such usage.
222
223  All major users of `securejoin.SecureJoin` already ensure that the paths they
224  provide are safe (and this is ultimately a question of user error), but
225  removing this foot-gun is probably a good idea. Of course, this is
226  necessarily a breaking API change (though we expect no real users to be
227  affected by it).
228
229  Thanks to [Erik Sjรถlund](https://github.com/eriksjolund), who initially
230  reported this issue as a possible security issue.
231
232- `MkdirAll` and `MkdirHandle` now take an `os.FileMode`-style mode argument
233  instead of a raw `unix.S_*`-style mode argument, which may cause compile-time
234  type errors depending on how you use `filepath-securejoin`. For most users,
235  there will be no change in behaviour aside from the type change (as the
236  bottom `0o777` bits are the same in both formats, and most users are probably
237  only using those bits).
238
239  However, if you were using `unix.S_ISVTX` to set the sticky bit with
240  `MkdirAll(Handle)` you will need to switch to `os.ModeSticky` otherwise you
241  will get a runtime error with this update. In addition, the error message you
242  will get from passing `unix.S_ISUID` and `unix.S_ISGID` will be different as
243  they are treated as invalid bits now (note that previously passing said bits
244  was also an error).
245
246## [0.3.6] - 2024-12-17 ##
247
248### Compatibility ###
249- The minimum Go version requirement for `filepath-securejoin` is now Go 1.18
250  (we use generics internally).
251
252  For reference, `filepath-securejoin@v0.3.0` somewhat-arbitrarily bumped the
253  Go version requirement to 1.21.
254
255  While we did make some use of Go 1.21 stdlib features (and in principle Go
256  versions <= 1.21 are no longer even supported by upstream anymore), some
257  downstreams have complained that the version bump has meant that they have to
258  do workarounds when backporting fixes that use the new `filepath-securejoin`
259  API onto old branches. This is not an ideal situation, but since using this
260  library is probably better for most downstreams than a hand-rolled
261  workaround, we now have compatibility shims that allow us to build on older
262  Go versions.
263- Lower minimum version requirement for `golang.org/x/sys` to `v0.18.0` (we
264  need the wrappers for `fsconfig(2)`), which should also make backporting
265  patches to older branches easier.
266
267## [0.3.5] - 2024-12-06 ##
268
269### Fixed ###
270- `MkdirAll` will now no longer return an `EEXIST` error if two racing
271  processes are creating the same directory. We will still verify that the path
272  is a directory, but this will avoid spurious errors when multiple threads or
273  programs are trying to `MkdirAll` the same path. opencontainers/runc#4543
274
275## [0.3.4] - 2024-10-09 ##
276
277### Fixed ###
278- Previously, some testing mocks we had resulted in us doing `import "testing"`
279  in non-`_test.go` code, which made some downstreams like Kubernetes unhappy.
280  This has been fixed. (#32)
281
282## [0.3.3] - 2024-09-30 ##
283
284### Fixed ###
285- The mode and owner verification logic in `MkdirAll` has been removed. This
286  was originally intended to protect against some theoretical attacks but upon
287  further consideration these protections don't actually buy us anything and
288  they were causing spurious errors with more complicated filesystem setups.
289- The "is the created directory empty" logic in `MkdirAll` has also been
290  removed. This was not causing us issues yet, but some pseudofilesystems (such
291  as `cgroup`) create non-empty directories and so this logic would've been
292  wrong for such cases.
293
294## [0.3.2] - 2024-09-13 ##
295
296### Changed ###
297- Passing the `S_ISUID` or `S_ISGID` modes to `MkdirAllInRoot` will now return
298  an explicit error saying that those bits are ignored by `mkdirat(2)`. In the
299  past a different error was returned, but since the silent ignoring behaviour
300  is codified in the man pages a more explicit error seems apt. While silently
301  ignoring these bits would be the most compatible option, it could lead to
302  users thinking their code sets these bits when it doesn't. Programs that need
303  to deal with compatibility can mask the bits themselves. (#23, #25)
304
305### Fixed ###
306- If a directory has `S_ISGID` set, then all child directories will have
307  `S_ISGID` set when created and a different gid will be used for any inode
308  created under the directory. Previously, the "expected owner and mode"
309  validation in `securejoin.MkdirAll` did not correctly handle this. We now
310  correctly handle this case. (#24, #25)
311
312## [0.3.1] - 2024-07-23 ##
313
314### Changed ###
315- By allowing `Open(at)InRoot` to opt-out of the extra work done by `MkdirAll`
316  to do the necessary "partial lookups", `Open(at)InRoot` now does less work
317  for both implementations (resulting in a many-fold decrease in the number of
318  operations for `openat2`, and a modest improvement for non-`openat2`) and is
319  far more guaranteed to match the correct `openat2(RESOLVE_IN_ROOT)`
320  behaviour.
321- We now use `readlinkat(fd, "")` where possible. For `Open(at)InRoot` this
322  effectively just means that we no longer risk getting spurious errors during
323  rename races. However, for our hardened procfs handler, this in theory should
324  prevent mount attacks from tricking us when doing magic-link readlinks (even
325  when using the unsafe host `/proc` handle). Unfortunately `Reopen` is still
326  potentially vulnerable to those kinds of somewhat-esoteric attacks.
327
328  Technically this [will only work on post-2.6.39 kernels][linux-readlinkat-emptypath]
329  but it seems incredibly unlikely anyone is using `filepath-securejoin` on a
330  pre-2011 kernel.
331
332### Fixed ###
333- Several improvements were made to the errors returned by `Open(at)InRoot` and
334  `MkdirAll` when dealing with invalid paths under the emulated (ie.
335  non-`openat2`) implementation. Previously, some paths would return the wrong
336  error (`ENOENT` when the last component was a non-directory), and other paths
337  would be returned as though they were acceptable (trailing-slash components
338  after a non-directory would be ignored by `Open(at)InRoot`).
339
340  These changes were done to match `openat2`'s behaviour and purely is a
341  consistency fix (most users are going to be using `openat2` anyway).
342
343[linux-readlinkat-emptypath]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=65cfc6722361570bfe255698d9cd4dccaf47570d
344
345## [0.3.0] - 2024-07-11 ##
346
347### Added ###
348- A new set of `*os.File`-based APIs have been added. These are adapted from
349  [libpathrs][] and we strongly suggest using them if possible (as they provide
350  far more protection against attacks than `SecureJoin`):
351
352   - `Open(at)InRoot` resolves a path inside a rootfs and returns an `*os.File`
353     handle to the path. Note that the handle returned is an `O_PATH` handle,
354     which cannot be used for reading or writing (as well as some other
355     operations -- [see open(2) for more details][open.2])
356
357   - `Reopen` takes an `O_PATH` file handle and safely re-opens it to upgrade
358     it to a regular handle. This can also be used with non-`O_PATH` handles,
359     but `O_PATH` is the most obvious application.
360
361   - `MkdirAll` is an implementation of `os.MkdirAll` that is safe to use to
362     create a directory tree within a rootfs.
363
364  As these are new APIs, they may change in the future. However, they should be
365  safe to start migrating to as we have extensive tests ensuring they behave
366  correctly and are safe against various races and other attacks.
367
368[libpathrs]: https://github.com/cyphar/libpathrs
369[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
370
371## [0.2.5] - 2024-05-03 ##
372
373### Changed ###
374- Some minor changes were made to how lexical components (like `..` and `.`)
375  are handled during path generation in `SecureJoin`. There is no behaviour
376  change as a result of this fix (the resulting paths are the same).
377
378### Fixed ###
379- The error returned when we hit a symlink loop now references the correct
380  path. (#10)
381
382## [0.2.4] - 2023-09-06 ##
383
384### Security ###
385- This release fixes a potential security issue in filepath-securejoin when
386  used on Windows ([GHSA-6xv5-86q9-7xr8][], which could be used to generate
387  paths outside of the provided rootfs in certain cases), as well as improving
388  the overall behaviour of filepath-securejoin when dealing with Windows paths
389  that contain volume names. Thanks to Paulo Gomes for discovering and fixing
390  these issues.
391
392### Fixed ###
393- Switch to GitHub Actions for CI so we can test on Windows as well as Linux
394  and MacOS.
395
396[GHSA-6xv5-86q9-7xr8]: https://github.com/advisories/GHSA-6xv5-86q9-7xr8
397
398## [0.2.3] - 2021-06-04 ##
399
400### Changed ###
401- Switch to Go 1.13-style `%w` error wrapping, letting us drop the dependency
402  on `github.com/pkg/errors`.
403
404## [0.2.2] - 2018-09-05 ##
405
406### Changed ###
407- Use `syscall.ELOOP` as the base error for symlink loops, rather than our own
408  (internal) error. This allows callers to more easily use `errors.Is` to check
409  for this case.
410
411## [0.2.1] - 2018-09-05 ##
412
413### Fixed ###
414- Use our own `IsNotExist` implementation, which lets us handle `ENOTDIR`
415  properly within `SecureJoin`.
416
417## [0.2.0] - 2017-07-19 ##
418
419We now have 100% test coverage!
420
421### Added ###
422- Add a `SecureJoinVFS` API that can be used for mocking (as we do in our new
423  tests) or for implementing custom handling of lookup operations (such as for
424  rootless containers, where work is necessary to access directories with weird
425  modes because we don't have `CAP_DAC_READ_SEARCH` or `CAP_DAC_OVERRIDE`).
426
427## 0.1.0 - 2017-07-19
428
429This is our first release of `github.com/cyphar/filepath-securejoin`,
430containing a full implementation with a coverage of 93.5% (the only missing
431cases are the error cases, which are hard to mocktest at the moment).
432
433[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.1...HEAD
434[0.6.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.0...v0.6.1
435[0.6.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.6.0
436[0.5.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...v0.5.2
437[0.5.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.5.1
438[0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0
439[0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1
440[0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0
441[0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6
442[0.3.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...v0.3.5
443[0.3.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.3...v0.3.4
444[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.2...v0.3.3
445[0.3.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.1...v0.3.2
446[0.3.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.0...v0.3.1
447[0.3.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.5...v0.3.0
448[0.2.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.2.5
449[0.2.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.3...v0.2.4
450[0.2.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.2...v0.2.3
451[0.2.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.1...v0.2.2
452[0.2.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.0...v0.2.1
453[0.2.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.1.0...v0.2.0