{"record":{"id":"436cc55882d5be84","repo":"puppetlabs/puppet","slug":"operation-not-permitted","errorCode":null,"errorMessage":"Operation not permitted","messagePattern":"Operation not permitted","errorType":"exception","errorClass":"Errno::EPERM","httpStatus":null,"severity":"error","filePath":"lib/puppet/file_system/windows.rb","lineNumber":98,"sourceCode":"      file_name = file_name.to_s # handle PathName\n      stat = begin\n        Puppet::Util::Windows::File.stat(file_name)\n      rescue\n        nil\n      end\n\n      # sigh, Ruby + Windows :(\n      if !stat\n        begin\n          ::File.unlink(file_name)\n        rescue\n          Dir.rmdir(file_name)\n        end\n      elsif stat.ftype == 'directory'\n        if Puppet::Util::Windows::File.symlink?(file_name)\n          Dir.rmdir(file_name)\n        else\n          raise Errno::EPERM, file_name\n        end\n      else\n        ::File.unlink(file_name)\n      end\n    end\n\n    file_names.length\n  end\n\n  def stat(path)\n    Puppet::Util::Windows::File.stat(path)\n  end\n\n  def lstat(path)\n    unless Puppet.features.manages_symlinks?\n      return Puppet::Util::Windows::File.stat(path)\n    end\n","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/file_system/windows.rb#L80-L116","documentation":"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.","triggerScenarios":"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.","commonSituations":"Cross-platform cleanup code that works on POSIX (where unlink semantics differ) but hits subdirectories on Windows; cache/temp pruning walking into nested dirs.","solutions":["Use directory-aware deletion: remove subdirectories with Dir.rmdir/recursive delete (FileUtils.rm_r) instead of file unlink","Reorder operations: delete directory contents that are files, then remove directories explicitly bottom-up","On Windows, test the path with File.directory? and branch before unlinking"],"exampleFix":"# before\nentries.each { |e| Puppet::FileSystem.unlink(File.join(dir, e)) } # raises EPERM on dirs (Windows)\n\n# after\nrequire 'fileutils'\nFileUtils.rm_r(dir)","handlingStrategy":"validation","validationCode":"entries = Dir.children(dir).map { |e| File.join(dir, e) }\nfiles, dirs = entries.partition { |p| File.file?(p) || File.symlink?(p) }\n# handle files with unlink APIs; handle dirs with rmdir/rm_r","typeGuard":"def unlinkable?(path)\n  File.file?(path) || File.symlink?(path) || (File.directory?(path) && File.symlink?(path))\nend","tryCatchPattern":"begin\n  Puppet::FileSystem.unlink(path)\nrescue Errno::EPERM\n  raise unless Gem.win_platform? && File.directory?(path) && !File.symlink?(path)\n  Dir.rmdir(path) # or FileUtils.rm_r for a populated tree\nend","preventionTips":["Branch on File.directory? before unlink on Windows","Prefer FileUtils.rm_r for whole trees instead of hand-rolled unlink walks","Test cleanup helpers on Windows CI, not only POSIX"],"tags":["puppet","windows","file-system","eperm","unlink"],"backgroundTag":"operation-not-permitted","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}