// Copyright 2014 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "util/posix/process_info.h"

#include <string.h>

#include <iterator>

#include "base/apple/mach_logging.h"
#include "base/logging.h"

namespace crashpad {

ProcessInfo::ProcessInfo() : kern_proc_info_(), initialized_() {
}

ProcessInfo::~ProcessInfo() {
}

bool ProcessInfo::InitializeWithPid(pid_t pid) {
  INITIALIZATION_STATE_SET_INITIALIZING(initialized_);

  int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
  size_t len = sizeof(kern_proc_info_);
  if (sysctl(mib, std::size(mib), &kern_proc_info_, &len, nullptr, 0) != 0) {
    PLOG(ERROR) << "sysctl for pid " << pid;
    return false;
  }

  // This sysctl does not return an error if the pid was not found. 10.9.5
  // xnu-2422.115.4/bsd/kern/kern_sysctl.c sysctl_prochandle() calls
  // xnu-2422.115.4/bsd/kern/kern_proc.c proc_iterate(), which provides no
  // indication of whether anything was done. To catch this, check that the PID
  // has changed from the 0 value it was given when initialized by the
  // constructor.
  if (kern_proc_info_.kp_proc.p_pid == 0) {
    LOG(WARNING) << "pid " << pid << " not found";
    return false;
  }

  DCHECK_EQ(kern_proc_info_.kp_proc.p_pid, pid);

  INITIALIZATION_STATE_SET_VALID(initialized_);
  return true;
}

bool ProcessInfo::InitializeWithTask(task_t task) {
  pid_t pid;
  kern_return_t kr = pid_for_task(task, &pid);
  if (kr != KERN_SUCCESS) {
    MACH_LOG(ERROR, kr) << "pid_for_task";
    return false;
  }

  return InitializeWithPid(pid);
}

pid_t ProcessInfo::ProcessID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_proc.p_pid;
}

pid_t ProcessInfo::ParentProcessID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_eproc.e_ppid;
}

uid_t ProcessInfo::RealUserID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_eproc.e_pcred.p_ruid;
}

uid_t ProcessInfo::EffectiveUserID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_eproc.e_ucred.cr_uid;
}

uid_t ProcessInfo::SavedUserID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_eproc.e_pcred.p_svuid;
}

gid_t ProcessInfo::RealGroupID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_eproc.e_pcred.p_rgid;
}

gid_t ProcessInfo::EffectiveGroupID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_eproc.e_ucred.cr_gid;
}

gid_t ProcessInfo::SavedGroupID() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_eproc.e_pcred.p_svgid;
}

std::set<gid_t> ProcessInfo::SupplementaryGroups() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);

  const short ngroups = kern_proc_info_.kp_eproc.e_ucred.cr_ngroups;
  DCHECK_GE(ngroups, 0);
  DCHECK_LE(static_cast<size_t>(ngroups),
            std::size(kern_proc_info_.kp_eproc.e_ucred.cr_groups));

  const gid_t* groups = kern_proc_info_.kp_eproc.e_ucred.cr_groups;
  return std::set<gid_t>(&groups[0], &groups[ngroups]);
}

std::set<gid_t> ProcessInfo::AllGroups() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);

  std::set<gid_t> all_groups = SupplementaryGroups();
  all_groups.insert(RealGroupID());
  all_groups.insert(EffectiveGroupID());
  all_groups.insert(SavedGroupID());
  return all_groups;
}

bool ProcessInfo::DidChangePrivileges() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_proc.p_flag & P_SUGID;
}

bool ProcessInfo::Is64Bit() const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  return kern_proc_info_.kp_proc.p_flag & P_LP64;
}

bool ProcessInfo::StartTime(timeval* start_time) const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);
  *start_time = kern_proc_info_.kp_proc.p_starttime;
  return true;
}

bool ProcessInfo::Arguments(std::vector<std::string>* argv) const {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);

  // The format of KERN_PROCARGS2 is explained in 10.9.2 adv_cmds-153/ps/print.c
  // getproclline(). It is an int (argc) followed by the executable’s string
  // area. The string area consists of NUL-terminated strings, beginning with
  // the executable path, and then starting on an aligned boundary, all of the
  // elements of argv, envp, and applev.

  // It is possible for a process to exec() in between the two sysctl() calls
  // below. If that happens, and the string area of the new program is larger
  // than that of the old one, args_size_estimate will be too small. To detect
  // this situation, the second sysctl() attempts to fetch args_size_estimate +
  // 1 bytes, expecting to only receive args_size_estimate. If it gets the extra
  // byte, it indicates that the string area has grown, and the sysctl() pair
  // will be retried a limited number of times.

  size_t args_size_estimate;
  size_t args_size;
  std::string args;
  int tries = 3;
  const pid_t pid = ProcessID();
  do {
    int mib[] = {CTL_KERN, KERN_PROCARGS2, pid};
    int rv =
        sysctl(mib, std::size(mib), nullptr, &args_size_estimate, nullptr, 0);
    if (rv != 0) {
      PLOG(ERROR) << "sysctl (size) for pid " << pid;
      return false;
    }

    // TODO(https://crashpad.chromium.org/bug/355): This was increased from + 1
    // to + 32 to work around a new bug in macOS 11.0db6 20A5364e that has
    // broken {CTL_KERN, KERN_PROCARGS2} such that it will not work properly
    // unless provided with a buffer at least 17 bytes larger than indicated in
    // args_size_estimate. If this bug is fixed prior to the 11.0 release,
    // remove the workaround and go back to + 1. (A positive offset is needed
    // for the reasons described above.)
    args_size = args_size_estimate + 32;
    args.resize(args_size);
    rv = sysctl(mib, std::size(mib), &args[0], &args_size, nullptr, 0);
    if (rv != 0) {
      PLOG(ERROR) << "sysctl (data) for pid " << pid;
      return false;
    }
  } while (args_size == args_size_estimate + 1 && tries--);

  if (args_size == args_size_estimate + 1) {
    LOG(ERROR) << "unexpected args_size";
    return false;
  }

  // KERN_PROCARGS2 needs to at least contain argc.
  if (args_size < sizeof(int)) {
    LOG(ERROR) << "tiny args_size";
    return false;
  }
  args.resize(args_size);

  // Get argc.
  int argc;
  memcpy(&argc, &args[0], sizeof(argc));

  // Find the end of the executable path.
  size_t start_pos = sizeof(argc);
  size_t nul_pos = args.find('\0', start_pos);
  if (nul_pos == std::string::npos) {
    LOG(ERROR) << "unterminated executable path";
    return false;
  }

  // Find the beginning of the string area.
  start_pos = args.find_first_not_of('\0', nul_pos);
  if (start_pos == std::string::npos) {
    LOG(ERROR) << "no string area";
    return false;
  }

  std::vector<std::string> local_argv;
  while (argc-- && nul_pos != std::string::npos) {
    nul_pos = args.find('\0', start_pos);
    local_argv.push_back(args.substr(start_pos, nul_pos - start_pos));
    start_pos = nul_pos + 1;
  }

  if (argc >= 0) {
    // Not every argument was recovered.
    LOG(ERROR) << "did not recover all arguments";
    return false;
  }

  argv->swap(local_argv);
  return true;
}

}  // namespace crashpad
