puppetlabs/puppet · error · Errno::EPERM

Operation not permitted

Error message

Operation not permitted

What it means

In Puppet's Windows file-system layer, recursive unlink logic dispatches per stat type: regular files go to File.unlink, symlinked directories to Dir.rmdir, but a real (non-symlink) directory cannot be removed with file semantics, so it raises Errno::EPERM with the file name. This is a guard against half-deleting a tree the caller meant to handle as directories.

Source

Thrown at lib/puppet/file_system/windows.rb:98

      file_name = file_name.to_s # handle PathName
      stat = begin
        Puppet::Util::Windows::File.stat(file_name)
      rescue
        nil
      end

      # sigh, Ruby + Windows :(
      if !stat
        begin
          ::File.unlink(file_name)
        rescue
          Dir.rmdir(file_name)
        end
      elsif stat.ftype == 'directory'
        if Puppet::Util::Windows::File.symlink?(file_name)
          Dir.rmdir(file_name)
        else
          raise Errno::EPERM, file_name
        end
      else
        ::File.unlink(file_name)
      end
    end

    file_names.length
  end

  def stat(path)
    Puppet::Util::Windows::File.stat(path)
  end

  def lstat(path)
    unless Puppet.features.manages_symlinks?
      return Puppet::Util::Windows::File.stat(path)
    end

View on GitHub (pinned to e227c27540)

Solutions

  1. Use directory-aware deletion: remove subdirectories with Dir.rmdir/recursive delete (FileUtils.rm_r) instead of file unlink
  2. Reorder operations: delete directory contents that are files, then remove directories explicitly bottom-up
  3. On Windows, test the path with File.directory? and branch before unlinking

Example fix

# before
entries.each { |e| Puppet::FileSystem.unlink(File.join(dir, e)) } # raises EPERM on dirs (Windows)

# after
require 'fileutils'
FileUtils.rm_r(dir)
Defensive patterns

Strategy: validation

Validate before calling

entries = Dir.children(dir).map { |e| File.join(dir, e) }
files, dirs = entries.partition { |p| File.file?(p) || File.symlink?(p) }
# handle files with unlink APIs; handle dirs with rmdir/rm_r

Type guard

def unlinkable?(path)
  File.file?(path) || File.symlink?(path) || (File.directory?(path) && File.symlink?(path))
end

Try / catch

begin
  Puppet::FileSystem.unlink(path)
rescue Errno::EPERM
  raise unless Gem.win_platform? && File.directory?(path) && !File.symlink?(path)
  Dir.rmdir(path) # or FileUtils.rm_r for a populated tree
end

Prevention

When it happens

Trigger: Invoking Puppet::FileSystem unlink/recursion APIs (e.g. clearing a directory's contents) on Windows where a subdirectory entry is encountered; deleting a mixed tree through a file-oriented API on Windows.

Common situations: Cross-platform cleanup code that works on POSIX (where unlink semantics differ) but hits subdirectories on Windows; cache/temp pruning walking into nested dirs.

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/436cc55882d5be84. Report an issue: GitHub.