pathrs 0.2.4

C-friendly API to make path resolution safer on Linux.
Documentation
#!/usr/bin/bats -t
# SPDX-License-Identifier: MPL-2.0
#
# libpathrs: safe path resolution on Linux
# Copyright (C) 2019-2025 SUSE LLC
# Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

load helpers

function setup() {
	setup_tmpdirs
}

function teardown() {
	teardown_tmpdirs
}

# TODO: All of these tests are very limited because we cannot verify anything
# useful about the opened files (and the path is actually not guaranteed to be
# correct outside of magic-link cases). Ideally we would instead output
# something simple like the fdinfo, stat, and/or contents to verify against.

@test "procfs --base open --oflags O_RDONLY" {
	pathrs-cmd procfs open modules
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output"

	pathrs-cmd procfs --base root open --oflags O_RDONLY modules
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output"
}

@test "procfs --base pid=\$\$ --oflags O_RDONLY" {
	pathrs-cmd procfs open modules
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output"

	pathrs-cmd procfs --base root open --oflags O_RDONLY modules
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output"
}

@test "procfs --base self --oflags O_RDONLY" {
	pathrs-cmd procfs --base self open status
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/status$' <<<"$output"

	pathrs-cmd procfs --base self open stack
	[ "$status" -eq 0 ]
	# NOTE: While only admins can read from /proc/$n/stack, we can open it.
	grep -E '^FILE-PATH (/proc)?/[0-9]+/stack$' <<<"$output"
}

@test "procfs --base thread-self --oflags O_RDONLY" {
	pathrs-cmd procfs --base thread-self open status
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+/status$' <<<"$output"

	pathrs-cmd procfs --base thread-self open stack
	[ "$status" -eq 0 ]
	# NOTE: While only admins can read from /proc/$n/stack, we can open it.
	grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+/stack$' <<<"$output"
}

# Make sure that thread-self and self are actually handled differently.
@test "procfs open [self != thread-self]" {
	pathrs-cmd procfs --base self open task
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]*/task$' <<<"$output"

	pathrs-cmd procfs --base thread-self open task
	check-errno ENOENT
}

@test "procfs open --follow [symlinks]" {
	pathrs-cmd procfs open mounts
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/mounts$' <<<"$output"

	pathrs-cmd procfs open --oflags O_DIRECTORY mounts
	check-errno ENOTDIR

	pathrs-cmd procfs open --oflags O_DIRECTORY net
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/net$' <<<"$output"

	pathrs-cmd procfs open --follow self
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+$' <<<"$output"

	pathrs-cmd procfs open --follow thread-self
	if [ -e /proc/thread-self ]; then
		[ "$status" -eq 0 ]
		grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+$' <<<"$output"
	else
		check-errno ENOENT
	fi
}

@test "procfs open --follow [magic-links]" {
	exepath="$(readlink -f "$PATHRS_CMD")"

	pathrs-cmd procfs --base self open --follow --oflags O_RDONLY exe
	[ "$status" -eq 0 ]
	! grep -E '^FILE-PATH (/proc)?/[0-9]+/.*$' <<<"$output"
	# We can only guess /proc/self/exe for compiled pathrs-cmd binaries.
	if is-compiled "$PATHRS_CMD"; then
		grep -Fx "FILE-PATH $exepath" <<<"$output"
	fi

	realpwd="$(readlink -f "$PWD")"

	pathrs-cmd procfs --base self open --follow --oflags O_RDONLY cwd
	[ "$status" -eq 0 ]
	grep -Fx "FILE-PATH $realpwd" <<<"$output"

	dummyfile="$(setup_tmpdir)/dummyfile"
	echo "THIS SHOULD BE TRUNCATED" >"$dummyfile"
	[ "$(stat -c '%s' "$dummyfile")" -ne 0 ]

	pathrs-cmd procfs --base thread-self open --follow --oflags O_RDWR,O_TRUNC fd/100 100>>"$dummyfile"
	[ "$status" -eq 0 ]
	grep -Fx "FILE-PATH $dummyfile" <<<"$output"
	# The file should've been truncated by O_TRUNC.
	[ "$(stat -c '%s' "$dummyfile")" -eq 0 ]
}

@test "procfs open --no-follow [symlinks]" {
	pathrs-cmd procfs open --no-follow mounts
	check-errno ELOOP

	pathrs-cmd procfs open --no-follow net
	check-errno ELOOP

	pathrs-cmd procfs open --no-follow self
	check-errno ELOOP

	pathrs-cmd procfs open --no-follow thread-self
	if [ -e /proc/thread-self ]; then
		check-errno ELOOP
	else
		check-errno ENOENT
	fi
}

@test "procfs open --no-follow [magic-links]" {
	pathrs-cmd procfs --base pid=1 open --no-follow --oflags O_DIRECTORY root
	# O_DIRECTORY beats O_NOFOLLOW!
	check-errno ENOTDIR

	pathrs-cmd procfs --base pid=1 open --no-follow --oflags O_RDWR root
	# O_NOFOLLOW beats permission errors
	check-errno ELOOP

	pathrs-cmd procfs --base self open --no-follow --oflags O_RDONLY exe
	check-errno ELOOP

	pathrs-cmd procfs --base thread-self open --no-follow --oflags O_RDONLY exe
	check-errno ELOOP

	dummyfile="$(setup_tmpdir)/dummyfile"
	echo "THIS SHOULD NOT BE TRUNCATED" >"$dummyfile"
	[ "$(stat -c '%s' "$dummyfile")" -ne 0 ]

	pathrs-cmd procfs --base thread-self open --no-follow --oflags O_RDWR,O_TRUNC fd/100 100>>"$dummyfile"
	check-errno ELOOP
	# The file should NOT have been truncated by O_TRUNC.
	[ "$(stat -c '%s' "$dummyfile")" -ne 0 ]
}

@test "procfs open --no-follow --oflags O_PATH [symlinks]" {
	pathrs-cmd procfs --base self open --no-follow --oflags O_PATH exe
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/exe$' <<<"$output"

	pathrs-cmd procfs --base pid=$$ open --oflags O_PATH,O_NOFOLLOW exe
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/'"$$"'/exe$' <<<"$output"

	pathrs-cmd procfs --base thread-self open --oflags O_PATH,O_NOFOLLOW exe
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+/exe$' <<<"$output"
}

@test "procfs open [symlink parent component]" {
	pathrs-cmd procfs --base root open self/status
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/status$' <<<"$output"

	pathrs-cmd procfs --base root open self/fdinfo/0
	[ "$status" -eq 0 ]
	grep -E '^FILE-PATH (/proc)?/[0-9]+/fdinfo/0$' <<<"$output"

	exepath="$(readlink -f "$PATHRS_CMD")"

	pathrs-cmd procfs --base root open self/exe
	[ "$status" -eq 0 ]
	! grep -E '^FILE-PATH (/proc)?/[0-9]+/.*$' <<<"$output"
	# We can only guess /proc/self/exe for compiled pathrs-cmd binaries.
	if is-compiled "$PATHRS_CMD"; then
		grep -Fx "FILE-PATH $exepath" <<<"$output"
	fi
}