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

Add restore handler for password input #1312

Merged
merged 1 commit into from
Sep 4, 2023

Conversation

grisu48
Copy link
Contributor

@grisu48 grisu48 commented Feb 6, 2023

This commit restores the terminal state in case the program is interrupted while being in password read mode. This ensures the terminal remains usable, also if the password input is being cancelled

This PR provides a solution for containers/podman#17372

Signed-off-by: Felix Niederwanger felix.niederwanger@suse.com

@grisu48
Copy link
Contributor Author

grisu48 commented Feb 6, 2023

I'm not sure why the DCO fails. The commit and PR message have both the required Signed-off-by footer appended 🤔

@Luap99
Copy link
Member

Luap99 commented Feb 6, 2023

The DCO is failing because you did not set your actual name for the git commit (git config user.name). But I don't think this is required for us, we can set the check manually to pass.

@grisu48
Copy link
Contributor Author

grisu48 commented Feb 6, 2023

The DCO is failing because you did not set your actual name for the git commit (git config user.name). But I don't think this is required for us, we can set the check manually to pass.

Ahhhhhhh, thanks. Fixed and force-pushed.

@grisu48 grisu48 force-pushed the password branch 2 times, most recently from 110399a to cad3633 Compare February 6, 2023 14:29
Copy link
Member

@Luap99 Luap99 left a comment

Choose a reason for hiding this comment

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

pkg/auth/auth.go Outdated Show resolved Hide resolved
pkg/auth/auth.go Outdated
if oldState != nil {
terminal.Restore(stdin, oldState)
}
os.Exit(1)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we want to call exit so far down in the stack. At least in podman we have handlers that depend on proper function, see libpod/shutdown.

Also I have no idea where this code is used besides podman, I think it might be better to return an error so that the caller can decide what to do with it.

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 how to handle this - In the podman repository I couldn't find any signal handlers for os.Interrupt except for machine.go, which also uses os.Exit(1).

I can certainly modify it such that getUserAndPass returns a fmt.Errorf("interrupted") but I'm not sure if this is the desired behaviour. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

I concur that we should return an error here.

Copy link
Member

@vrothberg vrothberg left a comment

Choose a reason for hiding this comment

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

Thanks for contributing!

It's probably worth moving the logic around terminal.ReadPassword into a separate function and update all callers (incl. pkg/ssh).

pkg/auth/auth.go Outdated
if oldState != nil {
terminal.Restore(stdin, oldState)
}
os.Exit(1)
Copy link
Member

Choose a reason for hiding this comment

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

I concur that we should return an error here.

@grisu48
Copy link
Contributor Author

grisu48 commented Feb 6, 2023

I've updated the PR where I refactored a ReadPassword function to utils/utils.go. There are some places that would need to be updated later on as well, as @vrothberg stated.
They are located in other repositories and require this PR first, but it can be easily done afterwards.

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

AFAICS this is just impossible to do with term.ReadPassword: the original sample only works because of os.Exit, which we don’t want to do.

Looking around a bit, I just can’t see any plausible solution (x/term does have some code that attempts to handle Ctrl-C, but 1) in ICANON mode, it would only work after pressing Enter, and 2) we would have to signal.Ignore(os.Interrupt), which is a disruptive operation that can’t be undone).

In the extreme, we could fork / spawn a subprocess. Ugh… I’m not even sure that is safe to do, and it would be so very ugly.

pkg/util/util.go Outdated Show resolved Hide resolved
pkg/util/util.go Outdated Show resolved Hide resolved
pkg/util/util.go Outdated Show resolved Hide resolved
pkg/util/util.go Outdated Show resolved Hide resolved
pkg/util/util.go Outdated Show resolved Hide resolved
pkg/util/util.go Outdated Show resolved Hide resolved
pkg/util/util.go Outdated Show resolved Hide resolved
@mtrmac
Copy link
Collaborator

mtrmac commented Feb 6, 2023

In the extreme, we could fork / spawn a subprocess. Ugh…

That wouldn’t, alone, be sufficient either, because Ctrl-C causes SIGINT to be sent to the whole foreground process group, including bot the child and the parent, so Podman would still need to arrange to ignore SIGINT in some reversible way. And Podman creating a separate process group is another can of worms… that would interfere with Ctrl-Z / fg / bg.

I wouldn’t entirely rule out the possibility of making all of that work, but the complexity (and dependency one of the most obscure aspects of POSIX — with Windows and other systems a whole another can of worms) makes it very attractive to me to suggest to just give up on this. Convincing users to use a more modern / featured shell that resets the terminal state before printing a prompt, or providing such patches to major shells, is starting to look easier than Podman handling this…


One, sort of, alternative is to try to make the os.Exit approach more viable, by explicitly introducing some kind of ”CLI parsing phase” to Podman, where Podman is not yet allowed to really do anything substantial (but what about user namespaces and reexec??), and providing only a ReadPassphraseInCLIParsingPhase that makes a lot of assumptions about the environment (it’s safe to call os.Exit; this is the only package so far to invoke os/signal; and the like).

But, again, ugh.

@grisu48 grisu48 changed the title Add workaround for no password echo Add restore handler for password input Feb 7, 2023
@grisu48 grisu48 marked this pull request as draft February 7, 2023 08:18
@grisu48
Copy link
Contributor Author

grisu48 commented Feb 7, 2023

Thanks for the review and the input! I see two paths on how this could be addresses, both have their own caveats:

  1. Don't stop the INTERRUPTED signal, just restore the terminal

Please see suggestion 1 in the PR. I'm catching SIGINT, restore the terminal and then re-emit the signal. This is IMHO the least invasive change, as it should not alter the existing behaviour much: It would only restore the terminal as intermittent step.

  1. Handle SIGINT gracefully, return an error from ReadPassword but leave the terminal reader active, and thus stdin blocked

This is ugly, but has the advantage, that podman can run it's clean-up routines, as suggested by @Luap99 and @vrothberg. Successive access to stdin would result in unintended behaviour though, so this might be better from the perspective of ReadPassword but introduces a tripping hazard.

Please see the current commit, where I added a suggestion for both methods.


How should we continue? I have a preference for solution 1, but would leave it to someone with far better insight to suggest on how to proceed.

Thank you!

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

I really think we can’t reasonably solve this locally in c/common.

90% of the problem is that golang.org/x/term.ReadPassword needs to be interruptible (maybe using a context.Context). That would make its existing implementation automatically take care of restoring the TTY configuration.

Then we would need to “only” care about turning a signal into a cancellation + error, and possibly turning the error back into a signal. That’s still not trivial — but having the cancellation feature would allow us to turn the signal into a cancellation+error using a very non-invasive signal.Notify which doesn’t interfere with anything else in the process, and to turn the error into a signal at the very top of the Podman call stack, where there is nothing else to interfere with.

So, would anyone care to propose this to …/x/term maintainers?

pkg/util/util.go Outdated Show resolved Hide resolved
pkg/util/util.go Outdated
term.Restore(fd, oldState)
}
signal.Stop(interruptChannel)
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would expect the goroutine to exit here instead of waiting on interruptChannel (and hoping that the SIGINT actually terminates the parent function, which is not guaranteed in presence of other code).

pkg/util/util.go Outdated
term.Restore(fd, oldState)
}
signal.Stop(interruptChannel)
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion 1:

This works in a top-level application; it’s fairly suspect in library code. E.g. if there is anything else registered via signal.Notify, it will now see the signal twice.

Also, even with this approach, I think we need to interrupt term.ReadPassword, because there could be some other party with a signal handler; in that case even this syscall.Kill might never terminate the term.ReadPassword.

The good thing about this approach is that the exit code of the process actually reflects the signal. (BTW that doesn’t actually happen directly here, there is still a Go signal handler installed, but it does ~the same thing, uninstalling the handler and re-sending the signal.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My main concern is that there is no way of determining if there are other signal handlers for SIGINT present that attempt to gracefully terminate the application. This signal handler re-emits SIGINT and kills them. I could not find anything in os/signal that allows us to check, if there are other handlers present.

This makes this solution undesirable, as it would kill possible other signal handlers, apart from your points.

pkg/util/util.go Outdated
Comment on lines 80 to 85
buf.buf, buf.err = term.ReadPassword(fd)
inputChannel <- buf
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion 2:

With this, term.ReadPassword continues running, consuming resources (meh), but potentially interfering: it continues to consume data from stdin, and it can eventually return and change the terminal state at any future time.

I can’t see how we can terminate the current implementation: fd must be a terminal (so that ioctls work); and I can’t see that a pending read(3) would be interrupted by a close(3).


A downside of turning the signal into an error is that the exit status of the process will not be traditionally correct. (In principle, we could turn it into a recognizable error, and turn that error into a signal at the very top of Podman.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. The only way of solving this would be to change term.ReadPassword to handle interrupts itself gracefully.

@mtrmac
Copy link
Collaborator

mtrmac commented Feb 7, 2023

(x/term does have some code that attempts to handle Ctrl-C,

My mistake: term.ReadPassword and term.Terminal.ReadPassword are completely independent implementations, and the quoted note refers to the latter, which we don’t use. (Using it would not help us anyway...)

@grisu48
Copy link
Contributor Author

grisu48 commented Feb 8, 2023

The issue is filed in golang/go#31180 and so far there has not been much activity or any visible attempts to solve this.

If we would get an interruptible term.ReadPassword implementation, then suggestion 2 could be changed to a clean library function.

Looks like we're hitting a wall here.

@mtrmac
Copy link
Collaborator

mtrmac commented Feb 8, 2023

My suggestion for what to try would be to file a PR directly at https://github.com/golang/term , to add something like ReadPasswordWithContext. But I have no idea whether it would be likely to be accepted, and it’s also not work I can prioritize doing myself.

@grisu48
Copy link
Contributor Author

grisu48 commented Feb 9, 2023

Thanks. I'll have a look and see what we can do. I leave this PR open and update it accordingly, so we keep track of the progress.

@grisu48
Copy link
Contributor Author

grisu48 commented Feb 14, 2023

I'm working on a prototype for ReadPasswordWithContext but am busy with other tasks as well, so this might take some time.

Also: This requires a Unix/Windows implementation on a rather low level, so this is a nontrivial task.

@mtrmac
Copy link
Collaborator

mtrmac commented Feb 14, 2023

Also: This requires a Unix/Windows implementation on a rather low level, so this is a nontrivial task.

From a very quick look it seems that it could be possible to wrap the Reader passed to readPasswordLine with one that checks whether a context was canceled, but I didn’t actually try implementing that so I might be very off-base.

@grisu48
Copy link
Contributor Author

grisu48 commented Feb 15, 2023

Also: This requires a Unix/Windows implementation on a rather low level, so this is a nontrivial task.

From a very quick look it seems that it could be possible to wrap the Reader passed to readPasswordLine with one that checks whether a context was canceled, but I didn’t actually try implementing that so I might be very off-base.

The problem with this approach is that a ongoing Read from the reader would block until the next character, even if the context is cancelled. I try to avoid this issue by implementing a nonblocking polling approach for this, which should in principle not have this limitation. Implementing this for all supported platforms, including Windows, is a however not as easy as I was hoping for AFAICS 🙂

@grisu48
Copy link
Contributor Author

grisu48 commented Mar 6, 2023

golang/term#12 as a first attempt to implement ReadPasswordWithContext. I would not expect this to fly soon, as the approach is not yet elegant enough for a library function. Still, the issue is not forgotten 🙂

@grisu48
Copy link
Contributor Author

grisu48 commented Mar 24, 2023

No reply for two weeks on golang/term#12. I also believe this is not going to fly because there two main issues that can not be resolved elegantly within the library function:

  1. Readers can't be interrupted. I solve this by non-blocking IO, but that's ugly and it does not work on Windows (hmpf!)
  2. Non-blocking IO requires polling, which or hogs the CPU or adds a (hopefully not noticeable) latency

I believe we're hitting a dead end there as well. Has anyone an alternative idea how we would approach this issue?
I would still argue, that having a signal handler which restores the terminal and then calls os.Exit might be the best of the bad options, given that this is a) AFAICS the current behaviour and b) missing alternatives.

What do you think?

@mtrmac
Copy link
Collaborator

mtrmac commented Mar 24, 2023

I think that leaves the ReadPassphraseInCLIParsingPhase approach… Aesthetically I find that quite unattractive, but I’ll leave it to Podman maintainers to decide whether it is is worth it.

@grisu48
Copy link
Contributor Author

grisu48 commented Aug 3, 2023

Reviving this PR.

Please review my current suggestion. After multiple attempts we concluded that there is no perfect solution, as ReadPassword from x/term leaves the terminal in non-echo mode (See golang/go#31180).

My suggestion based on #1312 (comment) is to implement a special ReadPassphraseInCLIParsingPhase function, which is allowed to terminate podman after restoring the terminal state. Since this function is only used in the authentication, where the current behaviour of podman is to terminate due to SIGINT, this would not alter the current program flow.

Other attempts to do busy waiting on a non-blocking terminal might work as well but have their own caveats (increased system load and latency due to busy polling). Also they don't work on Windows.

Asking @vrothberg @Luap99 on their opinion on this topic or if they have a better idea how to approach this issue.

terminal.Restore(fd, oldState)
}
signal.Reset(os.Interrupt)
os.Exit(1)
Copy link
Member

Choose a reason for hiding this comment

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

Why not just return an error and give Podman a chance to properly exit? There is usually some cleanup do be done before Podman exists.

Copy link
Contributor Author

@grisu48 grisu48 Aug 3, 2023

Choose a reason for hiding this comment

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

Because there is no way to return from terminal.ReadPassword, so this would mean the terminal remains blocked.

We could return from the function, but that still leaves the terminal blocked until the return key is pressed, as we discussed in #1312 (comment). This is indeed the main issue where we have been hitting a wall.

Copy link
Member

Choose a reason for hiding this comment

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

So if you returned the error, users would need to hit or if you exit here users have to hit correct?

Copy link
Contributor Author

@grisu48 grisu48 Aug 3, 2023

Choose a reason for hiding this comment

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

Not sure if I got your question right - If we return an error, the stdin would still be blocked by terminal.ReadPassword until the user hits return.

We even tried to close the fd but not even that unblocks the read call from terminal.ReadPassword . So returning an error here on interrupt is possible, but it would mean that the terminal (stdin) remains in a weird state, which is likely not usable on subsequent calls. 🙁

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 can easily rewrite it such that the function returns an error, but then stdin remains blocked. This would allow podman to do all the cleanup routines with the caveat that stdin cannot be used in the error case.

I decided to go the path of terminating within the function, because that's how podman currently handles the situation. From the functional standpoint this is the least intrusive change - the only difference is that the terminal is restored before podman is being terminated anyways - either via os.Exit(1) as in this PR or via SIGINT as it is of now.

How should we proceed?

Copy link
Member

Choose a reason for hiding this comment

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

man podman
...

       125 The error is with podman itself

              $ podman run --foo busybox; echo $?
              Error: unknown flag: --foo
              125

I guess it could be argued that killing Podman at this state is an error in Podman, so 125 is the correct error.
Other error codes usually come from the container or the lack of ability to execute a command within the container.

@mheon WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

The SIGINT handler in question should be https://github.com/containers/podman/blob/main/libpod/shutdown/handler.go but that doesn't help us much - that's nestled deep into Libpod and as such we can't touch it from here in c/common.

Generally speaking we should avoid os.Exit() whenever possible (for the exact reasons we wrote the shutdown handler in Libpod - there are situations where we need to keep Podman alive long enough to finish essential cleanup before obeying user-initiated shutdown), so I'm in favor of returning an error, but I have no easy way of disabling the interrupt handler from within c/common to give us consistent error codes.

Copy link
Contributor Author

@grisu48 grisu48 Aug 14, 2023

Choose a reason for hiding this comment

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

I see that almost everyone in this threads favours an approach of returning an error. This doesn't remove the race condition with libpod/shutdown/handler.go, but it works in most of the cases I tested (1/19), so it would improve the situation already substantially.

I will update the PR accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rhatdan @vrothberg @mheon Please review. The discussed race condition with libpod/shutdown/handler.go remains, but there is little what we can do within this function as signal handlers cannot be disabled an re-enabled. See also #1312 (comment)

On my laptop, only in 1/19 cases the race condition prevented the terminal from being restored, in the other 18 cases it works as expected. It's not ideal, but given a missing viable alternative I'd argue that it still is an improvement and the ReadPassword function behaves as desired.

Thanks for the patience!

Copy link
Member

Choose a reason for hiding this comment

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

So I think the answer here is to take a step back, I understand what this PR is trying to do and that looks reasonable.

If we look at the code path exactly podman login bypasses pretty much all of libpod and calls directly in to pkg/auth here. So shouldn't be the answer disable the libpod setup for podman login/logout in which case there cannot be a race between the signal handlers.

@rhatdan
Copy link
Member

rhatdan commented Aug 14, 2023

/approve
LGTM
@vrothberg @Luap99 @mtrmac PTAL

pkg/util/util_supported.go Outdated Show resolved Hide resolved
@grisu48 grisu48 force-pushed the password branch 2 times, most recently from 91a353f to b8fab13 Compare August 15, 2023 15:05
@rhatdan
Copy link
Member

rhatdan commented Aug 16, 2023

@giuseppe @Luap99 @vrothberg @mtrmac PTAL

This commit restores the terminal state in case the program is
interrupted while being in password read mode. This ensures the terminal
remains usable, also if the password input is being cancelled.

Signed-off-by: phoenix <felix.niederwanger@suse.com>
@grisu48
Copy link
Contributor Author

grisu48 commented Aug 17, 2023

Rebased and applied gofumpt.

@grisu48
Copy link
Contributor Author

grisu48 commented Sep 1, 2023

@rhatdan ping?

@rhatdan
Copy link
Member

rhatdan commented Sep 2, 2023

LGTM
@giuseppe @Luap99 @vrothberg @mtrmac PTAL

Copy link
Member

@vrothberg vrothberg left a comment

Choose a reason for hiding this comment

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

/lgtm

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Sep 4, 2023

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: grisu48, rhatdan, vrothberg

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-merge-robot openshift-merge-robot merged commit 034496e into containers:main Sep 4, 2023
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants