Skip to content

[RFC] Making libpathrs Migrations Easier #56

@cyphar

Description

@cyphar

While talking to folks about libpathrs, one common topic of concern is that switching to a CGo dependency for a library which is not (yet) widely packaged can be difficult, as well as some more general concerns about binary sizes (for the record, dylib builds of libpathrs are ~550K while static builds are ~2.7M, though after unused symbols are stripped after linking it might be less in practice).

It seems very likely that I will have to continue maintaining github.com/cyphar/filepath-securejoin for the foreseeable future, but at the same time there are plenty of reasons to move to libpathrs (more hardening against some attacks, cross-language compatibility, more features, better lifecycle of file objects, possibly non-Linux support in the future). I see a few potential options:

0. Abandon libpathrs

I'm mainly including this one for completeness, but it's a non-starter. The reason for writing libpathrs is that these kinds of path security issues exist all over the stack and so any library trying to solve the problem needs to be written in a language that has decent support for exporting an API via C, which isn't the case for Go.

There are also some deeper issues with Go (the lifecycle of *os.File being the most notable one) which actually make it preferable for the core implementation to be in a non-Go language, even if you end up providing Go bindings. For instance, the symlink stack implementation in libpathrs doesn't make any copies of the file descriptors (thanks to Rc and OwnedFd ownership model) but the Go implementation is forced to create copies of every symlink fd we encounter because the lookup code needs to close intermediate file handles.

1. "Just Suck It Up"

I continue to maintain github.com/cyphar/filepath-securejoin and libpathrs in parallel, and users wanting to switch to libpathrs will to switch their usage (which can be done in parallel, but still results in needless code bloat).

This is the "cleanest" approach if we want to separate the two projects, but it makes it quite unlikely that users will switch to libpathrs for a long time, and getting libpathrs packaged requires users for distributions to bother packaging libpathrs with no strong reason to. Also, lots of extra maintenance overhead, as there will be a push for features in libpathrs to be ported to filepath-securejoin (as has happened over the past year).

2. Use filepath-securejoin as a "Bridge" ("libpathrs" build tags in filepath-securejoin)

As the API in github.com/cyphar/filepath-securejoin is based quite directly on a (simplified) version of the libpathrs API, it seems that we could have two implementations of github.com/cyphar/filepath-securejoin. One will be the (current) pure Go implementation, and the other will be a dumb wrapper around libpathrs.

Then, users would be able to opt-in to libpathrs support at compile time by using -tags securejoin_libpathrs (suggestions for other names welcome). Because Go build tags are global, this has the added benefit of migrating all users of github.com/cyphar/filepath-securejoin to using libpathrs transparently at build time. Distributions will then be able to slowly migrate to libpathrs, while also supporting building on legacy platforms without Rust support.

Another option would be to make this opt-out (i.e. -tags securejoin_purego to use the current implementation). This would be ideal in a vacuum, but it would cause build errors for existing projects and so would require us to release github.com/cyphar/filepath-securejoin/v2 in order to avoid downstream breakages. We could do that, but then we would need to also use the former option to reduce the . Given that adding build tags is usually easier than removing them (when it comes to Makefiles) perhaps securejoin_libpathrs is a technically better solution anyway?

There are some obvious follow-up questions about testing these wrappers -- we might be able to just run go test -tags securejoin_libpathrs but our tests use internal functions a fair bit (though it might all "just work"...).

This option (with the opt-in -tags securejoin_libpathrs) is my current personal preference, as it doesn't increase maintainership burden but allows distributions to optionally enable libpathrs.

3. Migrate the filepath-securejoin implementations into libpathrs ("pure Go" build tags in libpathrs)

This is kind of a mirror proposal to (2). Instead of making filepath-securejoin a wrapper around libpathrs, we can copy the implementation of filepath-securejoin and make the Go bindings of libpathrs have a pure Go equivalent. This would be opt-in, so users would have to build with -tags libpathrs_purego.

The only real benefit this has in principle is that it unifies the pure Go and libpathrs implementations in one repo.

Ultimately the issue with this approach is that it has the main downsides of both (1) and (2). This doesn't help with maintaining filepath-securejoin unless I also make it a wrapper around libpathrs (and then the flag can't be opt-in unless I expose the internal Go implementation in a subpackage, which is also ugly). Also there will be a stronger push to continue to maintain the pure Go implementations when (in my view) they should just be a bridge towards users migrating to libpathrs. And it also means that we will have to maintain these wrappers in at least two repos, unless we decide to archive this repo (which is not tenable as we have quite a few users).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions