Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PUP-6675) Use pipes instead of temporary files for Puppet exec #5844

Merged
merged 2 commits into from
May 10, 2017

Conversation

MikaelSmith
Copy link
Contributor

Under selinux, when Puppet is invoked by another process with reduced
privileges, any sub-programs invoked by Puppet will not inherit Puppet's
selinux priveleges. This specifically causes silent failures when
invoking applications that don't normally have the ability to write
files - such as iptables-save or hostname - because Puppet redirects
their output to a temporary file.

Use pipes instead of a temporary file to capture the output of
subprocesses.

@MikaelSmith
Copy link
Contributor Author

I think this works, and it solves the problem I'm trying to fix. However, it's pretty fundamental and I'm still in the process of testing it.

@puppetcla
Copy link

CLA signed by all contributors.

@MikaelSmith MikaelSmith added the blocked PRs blocked on work external to the PR itself label May 5, 2017
@MikaelSmith MikaelSmith changed the title WIP (PUP-6675) Use pipes instead of temporary files for Puppet exec (PUP-6675) Use pipes instead of temporary files for Puppet exec May 5, 2017
@MikaelSmith
Copy link
Contributor Author

@MikaelSmith
Copy link
Contributor Author

Added some testing via pxp-agent and mcollective: puppetlabs/pxp-agent#583, https://github.com/puppetlabs/marionette-collective/pull/433

@@ -188,7 +188,8 @@ def self.execute(command, options = NoOptionsSpecified)

begin
stdin = Puppet::FileSystem.open(options[:stdinfile] || null_file, nil, 'r')
stdout = options[:squelch] ? Puppet::FileSystem.open(null_file, nil, 'w') : Puppet::FileSystem::Uniquefile.new('puppet')
reader, writer = IO.pipe
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the child needs to close the reader and the parent needs to close the writer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 226/227 should be closing the writer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

64KB test fails, need to read output in a loop to clear the buffer before waiting on the process.

@MikaelSmith MikaelSmith force-pushed the PUP-6675 branch 4 times, most recently from fd242cb to cec28d7 Compare May 5, 2017 20:22
@MikaelSmith
Copy link
Contributor Author

I believe this is ready. Kicked off a build at https://jenkins-master-prod-1.delivery.puppetlabs.net/view/puppet-agent/view/ad-hoc/view/vmpooler/job/platform_puppet-agent_pkg-van-ship_ad-hoc-vmpooler-ad-hoc/54/, which includes acceptance tests from this PR, the pxp-agent PR, and the mco PR.

@MikaelSmith
Copy link
Contributor Author

Manually ran mco tests on RedHat 7

      Test Suite: tests @ 2017-05-05 15:07:30 -0700

      - Host Configuration Summary -


              - Test Case Summary for suite 'tests' -
       Total Suite Time: 156.45 seconds
      Average Test Time: 26.08 seconds
              Attempted: 6
                 Passed: 5
                 Failed: 0
                Errored: 0
                Skipped: 1
                Pending: 0
                  Total: 6

      - Specific Test Case Status -

Failed Tests Cases:
Errored Tests Cases:
Skipped Tests Cases:
    Test Case tests/mco_puppet_powershell.rb
Pending Tests Cases:

@MikaelSmith
Copy link
Contributor Author

pxp-agent and mcollective tests failed on a few platforms due to test setup. I've fixed them, but verifying they now where can be handled after merging this fix.

@MikaelSmith MikaelSmith removed the blocked PRs blocked on work external to the PR itself label May 8, 2017
@MikaelSmith
Copy link
Contributor Author

pxp-agent's run_puppet_killed_puppet test seems to be a real issue caused by this change. Looking into it.

@Iristyle
Copy link
Contributor

Iristyle commented May 8, 2017

Wow, looks like the original Windows / Posix split happened in cb53870

Though the original Tempfile usage stems from f8c1b08

# There are problems with read blocking with badly behaved children
# read.partialread doesn't seem to capture either stdout or stderr
# We hack around this using a temporary file

@MikaelSmith
Copy link
Contributor Author

The original Tempfile usage is actually 30ebbc9.

Looks like they attempted to fix it, which led to https://projects.puppetlabs.com/issues/3025.

@MikaelSmith
Copy link
Contributor Author

MikaelSmith commented May 8, 2017

Using pipes on Windows seems to cause problems when we test the lockfile after Puppet is killed. Process.kill(0, pid) continues to return 1 until the subprocess exits.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
EOF
on(agent, "for i in {1..10}; do cat #{testfile} #{testfile} > #{testfile}.2 && mv #{testfile}.2 #{testfile}; done")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just generate 64k+ content on the Ruby side? It will send more over the wire, but will remove the Bash-ism above. There will be a QA undertaking shortly to prune / rewrite tests where possible. The less Bash shows up in tests, the better I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I can rewrite that.

elsif Puppet.features.posix?
child_pid = nil
begin
child_pid = execute_posix(*exec_args)
[stdin, stdout, stderr].each {|io| io.close rescue nil}
unless options[:squelch]
while !reader.eof?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe read is normally performed within a Ruby Thread since it's blocking.

You can look at the capture2 source for an example - https://github.com/ruby/ruby/blob/trunk/lib/open3.rb#L305-L322

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're only using a single pipe, I think that's unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically, capture2 is using a thread for read while it also writes to stdin. We're not handling anything except reading from a single pipe, so only a single thread needed.

@@ -187,18 +187,30 @@ def self.execute(command, options = NoOptionsSpecified)
null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null'

begin
reader, writer = IO.pipe unless options[:squelch]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a pipe on Windows causes an issue in https://github.com/puppetlabs/pxp-agent/blob/master/acceptance/tests/pxp-module-puppet/run_puppet_killed_puppet.rb.

When that test kills the Puppet process, the child process is intentionally left running. However, the mechanism Puppet uses to determine whether a previous Puppet lockfile is stale reads the Puppet run as active. This appears to be because it's terminated, but still has an open handle to the pipe with the child process (perhaps specifically that it's waiting to read from that pipe).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what to do about that. It's not ideal to be unable to run Puppet again if a previous run was killed but left a long-running or hung process behind, but I'm not sure when that would really come up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Puppet uses Ruby's Process.kill to check whether the process is still running, and that states the process is running if OpenProcess succeeds. Which it appears to as long as the child process has an open handle to the shared pipe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@MikaelSmith MikaelSmith May 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is behavior people depend on in Puppet, so I modified the pxp-agent test to not expect it in puppetlabs/pxp-agent#583.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Iristyle have any concerns here?

@MikaelSmith
Copy link
Contributor Author

MikaelSmith commented May 8, 2017

@MikaelSmith
Copy link
Contributor Author

Solaris machine died during Puppet acceptance tests, but everything else passed.

"C:/#{@testfilename}"
else
"/tmp/#{@testfilename}"
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use host.tmpfile which IIRC will return a platform-specific path that you can use later in the test. Might also add a teardown for deleting the file.

Puppet::Util::Execution.execute('test command')

expect(Puppet::FileSystem.exist?(path)).to be_falsey
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a test to make sure both ends of the pipe are closed (during normal run and if an exception is raised)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try, that seems useful.


if execution_stub = Puppet::Util::ExecutionStub.current_value
return execution_stub.call(*exec_args)
child_pid = execution_stub.call(*exec_args)
[stdin, stdout, stderr].each {|io| io.close rescue nil}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Puppetserver installs an execution stub, e.g. when executing an autosigning policy. Will it work correctly if we close these descriptors? /cc @camlow325

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should only be closing them on the Puppet side, the forked process inherits the handles. I'm actually not sure we need to close them earlier than we did before, since nothing else about forking changed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already do close them in Puppet Server, although it seems like it would have better if we didn't have to. See this code.

If we add this here, we might get exceptions when the second set of close calls are made. That should be okay with the swallowing rescue, though, right? Not sure if anything would be written to the log when this happens?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rescue nil should handle that in both cases, and nothing should be logged. The execution doesn't change at all in this case, after execution_stub.call we still would have closed all the handles at old line 226.

unless options[:squelch]
output = wait_for_output(stdout)
Puppet.warning "Could not get output" unless output
unless options[:squelch] || read_succeeded
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is read_succeeded necessary? If options[:squelch] is false, then we always call (for posix and windows):

while !reader.eof?
  output << reader.read
end
read_succeeded = true

which always sets read_succeeded to true or will raise. But if it's the latter, then we never reach this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be a rescue, so I agree I don't see any circumstance where we'd reach this line but reading had failed.

Copy link
Contributor Author

@MikaelSmith MikaelSmith May 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might make more sense outside the last ensure block. Update: nevermind, still no rescue that would cause it to trigger.

stdout.close!
if !options[:squelch] && reader
# if we opened a pipe, we need to clean it up.
reader.close
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an exception is raised between the time we open the pipe and when the execute_{posix,windows} call returns, then the writer is never closed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're misreading what block this ensure relates to. It starts at line 189.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an exception is raised between lines 190 and 201 (posix) or 207 (windows), then I don't think we close the writer:

begin
  reader, writer = IO.pipe unless options[:squelch]
  ...
  process_info = execute_windows(*exec_args) # if this raises
  begin
    [stdin, stdout, stderr].each {|io| io.close rescue nil} # this line is never reached
    ...
   end
ensure
  if !options[:squelch] && reader
    reader.close
  end
end

A good test is to try to execute a non-existing program with squelch = false. On Windows, I know execute_windows will raise, and I think the posix impl behaves the same.

Copy link
Contributor Author

@MikaelSmith MikaelSmith May 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does seem like a loose end. However, as far as I can tell if we never fork and then close one end, the other end also closes. So in that scenario the pipe would still be closed.

That still leaves dangling handles for the files. We could attempt to close all handles in the ensure block anyway.

@MikaelSmith
Copy link
Contributor Author

MikaelSmith commented May 9, 2017

@Iristyle
Copy link
Contributor

Iristyle commented May 9, 2017

Do we want this on stable or master @MikaelSmith ?

@MikaelSmith
Copy link
Contributor Author

I was hoping stable.

@joshcooper
Copy link
Contributor

@MikaelSmith In talking this over with the team, it feels too risky of a change to make in a .z without some assurances it won't cause regressions on other platforms. Can we target this for master, and if there are no downstream issues, backport to stable?

@MikaelSmith
Copy link
Contributor Author

Sure. I'll rebase and retarget.

This test passes lots of data over pipes when performing an exec, to
attempt to catch any issues with buffered reads over pipes. On *nix the
standand buffer size is 64KB.
Under selinux, when Puppet is invoked by another process with reduced
privileges, any sub-programs invoked by Puppet will not inherit Puppet's
selinux priveleges. This specifically causes silent failures when
invoking applications that don't normally have the ability to write
files - such as iptables-save or hostname - because Puppet redirects
their output to a temporary file.

Use pipes instead of a temporary file to capture the output of
subprocesses.
@MikaelSmith MikaelSmith changed the base branch from stable to master May 10, 2017 21:14
@MikaelSmith
Copy link
Contributor Author

Done. I'll retarget the pxp-agent and mco acceptance tests to master as well.

@MikaelSmith MikaelSmith reopened this May 10, 2017
@MikaelSmith
Copy link
Contributor Author

Not sure why it pulled in extra commits for commit checking, but everything else was passing.

@Magisus
Copy link
Contributor

Magisus commented May 10, 2017

Yeah looks like it didn't update the job parameter for some reason... weird.

@joshcooper
Copy link
Contributor

I think the failure is because we're using double-dots in the commit check, so it's pulling in commits on the base branch that were made after you forked this branch, and it looks like the "Revert" commits are causing issues:

Checking commits 054989b458a3a48188551bf8efbf855cb767a201..6519360061e2a005b0b5913f2789ea15fe792842

I thought we had updated travis and appveyor to use triple dots?

@Magisus
Copy link
Contributor

Magisus commented May 10, 2017

Weirdly, it kind of looks like we're doing the opposite: https://github.com/puppetlabs/puppet/blob/master/Rakefile#L86. However, I think in this case it was just Github's PR re-targeting being kind of screwy with commit hooks. We've seen stuff like that a lot.

Copy link
Contributor

@joshcooper joshcooper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waiting on appveyor

@Iristyle
Copy link
Contributor

Reverted in #5868

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants