fix: filter directory-only Landlock rights for non-directory paths (#17)
This commit is contained in:
@@ -246,19 +246,6 @@ func TestLinux_NetworkBlocksCurl(t *testing.T) {
|
|||||||
assertNetworkBlocked(t, result)
|
assertNetworkBlocked(t, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLinux_NetworkBlocksWget verifies that wget cannot reach the network.
|
|
||||||
func TestLinux_NetworkBlocksWget(t *testing.T) {
|
|
||||||
skipIfAlreadySandboxed(t)
|
|
||||||
skipIfCommandNotFound(t, "wget")
|
|
||||||
|
|
||||||
workspace := createTempWorkspace(t)
|
|
||||||
cfg := testConfigWithWorkspace(workspace)
|
|
||||||
|
|
||||||
result := runUnderSandboxWithTimeout(t, cfg, "wget -q --timeout=2 -O /dev/null http://example.com", workspace, 10*time.Second)
|
|
||||||
|
|
||||||
assertBlocked(t, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLinux_NetworkBlocksPing verifies that ping cannot reach the network.
|
// TestLinux_NetworkBlocksPing verifies that ping cannot reach the network.
|
||||||
func TestLinux_NetworkBlocksPing(t *testing.T) {
|
func TestLinux_NetworkBlocksPing(t *testing.T) {
|
||||||
skipIfAlreadySandboxed(t)
|
skipIfAlreadySandboxed(t)
|
||||||
@@ -482,6 +469,41 @@ func TestLinux_ProcSelfEnvReadable(t *testing.T) {
|
|||||||
assertAllowed(t, result)
|
assertAllowed(t, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLinux_GlobPatternAllowsWriteToMatchingFile verifies that glob patterns
|
||||||
|
// like "~/.claude*" correctly allow writes to matching files (not just directories).
|
||||||
|
// The bug was that Landlock rules for files were silently failing because
|
||||||
|
// directory-only access rights (MAKE_*, REFER, etc.) were being applied.
|
||||||
|
func TestLinux_GlobPatternAllowsWriteToMatchingFile(t *testing.T) {
|
||||||
|
skipIfAlreadySandboxed(t)
|
||||||
|
|
||||||
|
workspace := createTempWorkspace(t)
|
||||||
|
|
||||||
|
testFile := filepath.Join(workspace, ".testglob.json")
|
||||||
|
if err := os.WriteFile(testFile, []byte(`{"initial": true}`), 0o600); err != nil {
|
||||||
|
t.Fatalf("failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure allowWrite with a glob pattern that matches the file
|
||||||
|
cfg := testConfigWithWorkspace(workspace)
|
||||||
|
cfg.Filesystem.AllowWrite = []string{
|
||||||
|
workspace,
|
||||||
|
filepath.Join(workspace, ".testglob*"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to append to the file (shouldn't fail)
|
||||||
|
result := runUnderSandbox(t, cfg, "echo 'appended' >> "+testFile, workspace)
|
||||||
|
|
||||||
|
assertAllowed(t, result)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(testFile) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(content), "appended") {
|
||||||
|
t.Errorf("expected file to contain 'appended', got: %s", string(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper functions
|
// Helper functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -323,9 +323,48 @@ func (l *LandlockRuleset) addPathRule(path string, access uint64) error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = unix.Close(fd) }()
|
defer func() { _ = unix.Close(fd) }()
|
||||||
|
|
||||||
|
// Use fstat on the fd to avoid TOCTOU race between stat and open
|
||||||
|
var stat unix.Stat_t
|
||||||
|
if err := unix.Fstat(fd, &stat); err != nil {
|
||||||
|
if l.debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence:landlock] Failed to fstat path %s: %v\n", absPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
isDir := (stat.Mode & unix.S_IFMT) == unix.S_IFDIR
|
||||||
|
|
||||||
// Intersect with handled access to avoid invalid combinations
|
// Intersect with handled access to avoid invalid combinations
|
||||||
access &= l.getHandledAccessFS()
|
access &= l.getHandledAccessFS()
|
||||||
|
|
||||||
|
// Filter out directory-only access rights for non-directory paths (files, sockets, devices, etc.).
|
||||||
|
// Landlock returns EINVAL if you try to add MAKE_*, REMOVE_*, READ_DIR, or REFER rights to a non-directory.
|
||||||
|
// See: https://docs.kernel.org/userspace-api/landlock.html
|
||||||
|
// File-compatible rights: EXECUTE, WRITE_FILE, READ_FILE, TRUNCATE
|
||||||
|
// Directory-only rights: READ_DIR, REMOVE_*, MAKE_*, REFER
|
||||||
|
if !isDir {
|
||||||
|
dirOnlyRights := uint64(
|
||||||
|
LANDLOCK_ACCESS_FS_READ_DIR |
|
||||||
|
LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
||||||
|
LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
||||||
|
LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
||||||
|
LANDLOCK_ACCESS_FS_MAKE_DIR |
|
||||||
|
LANDLOCK_ACCESS_FS_MAKE_REG |
|
||||||
|
LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
||||||
|
LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
||||||
|
LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
||||||
|
LANDLOCK_ACCESS_FS_MAKE_SYM |
|
||||||
|
LANDLOCK_ACCESS_FS_REFER,
|
||||||
|
)
|
||||||
|
access &^= dirOnlyRights
|
||||||
|
}
|
||||||
|
|
||||||
|
if access == 0 {
|
||||||
|
if l.debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence:landlock] Skipping %s: no applicable access rights\n", absPath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
attr := landlockPathBeneathAttr{
|
attr := landlockPathBeneathAttr{
|
||||||
allowedAccess: access,
|
allowedAccess: access,
|
||||||
parentFd: int32(fd), //nolint:gosec // fd from unix.Open fits in int32
|
parentFd: int32(fd), //nolint:gosec // fd from unix.Open fits in int32
|
||||||
|
|||||||
Reference in New Issue
Block a user