address-book-cmd: ignore completion above 100 items

Avoid aerc from consuming all memory on the system if an address book
command returns 12 million addresses.

Read at most the first 100 lines and kill the command if it has not
finished. Display a warning in the logs for good measure.

The command is now assigned an different PGID (equal to its PID) to
allow killing it *and* all of its children. When the address book
command is a shell script that forks a process which never exits, it
will avoid killing the shell process and leaving its children without
parents.

Signed-off-by: Robin Jarry <robin@jarry.cc>
Tested-by: Bence Ferdinandy <bence@ferdinandy.com>
This commit is contained in:
Robin Jarry 2022-11-01 13:16:21 +01:00
parent ae99f4c5bb
commit 7565a96525

View file

@ -10,7 +10,9 @@ import (
"os/exec" "os/exec"
"regexp" "regexp"
"strings" "strings"
"syscall"
"git.sr.ht/~rjarry/aerc/logging"
"github.com/google/shlex" "github.com/google/shlex"
) )
@ -71,6 +73,10 @@ func isAddressHeader(h string) bool {
return false return false
} }
const maxCompletionLines = 100
var tooManyLines = fmt.Errorf("returned more than %d lines", maxCompletionLines)
// completeAddress uses the configured address book completion command to fetch // completeAddress uses the configured address book completion command to fetch
// completions for the specified string, returning a slice of completions and // completions for the specified string, returning a slice of completions and
// a prefix to be prepended to the selected completion, or an error. // a prefix to be prepended to the selected completion, or an error.
@ -88,6 +94,8 @@ func (c *Completer) completeAddress(s string) ([]string, string, error) {
if err != nil { if err != nil {
return nil, "", fmt.Errorf("stderr: %w", err) return nil, "", fmt.Errorf("stderr: %w", err)
} }
// reset the process group id to allow killing all its children
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return nil, "", fmt.Errorf("cmd start: %w", err) return nil, "", fmt.Errorf("cmd start: %w", err)
} }
@ -99,6 +107,12 @@ func (c *Completer) completeAddress(s string) ([]string, string, error) {
completions, err := readCompletions(stdout) completions, err := readCompletions(stdout)
if err != nil { if err != nil {
// make sure to kill the process *and* all its children
//nolint:errcheck // who cares?
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
logging.Warnf("command %s killed: %s", cmd, err)
}
if err != nil && !errors.Is(err, tooManyLines) {
buf, _ := io.ReadAll(stderr) buf, _ := io.ReadAll(stderr)
msg := strings.TrimSpace(string(buf)) msg := strings.TrimSpace(string(buf))
if msg != "" { if msg != "" {
@ -168,6 +182,9 @@ func readCompletions(r io.Reader) ([]string, error) {
return nil, fmt.Errorf("could not decode MIME string: %w", err) return nil, fmt.Errorf("could not decode MIME string: %w", err)
} }
completions = append(completions, decoded) completions = append(completions, decoded)
if len(completions) >= maxCompletionLines {
return completions, tooManyLines
}
} }
} }