Compare commits

..

10 Commits
v30.0 ... v30.1

Author SHA1 Message Date
topjohnwu
78f7fa348e Release Magisk v30.1
[skip ci]
2025-07-03 03:10:10 -07:00
pndwal
d8c448b99d Update faq.md
Add information on restoring Magisk App functionality when stub and full apps conflict.
2025-07-03 02:51:01 -07:00
topjohnwu
d4b83b6a44 Fix app compilation 2025-07-03 02:42:08 -07:00
vvb2060
e5d36d1d24 app: support config restrict policy 2025-07-03 02:42:08 -07:00
vvb2060
ff18cb8e70 su: support drop capabilities 2025-07-03 02:42:08 -07:00
topjohnwu
37a9724a54 Apply clippy fix 2025-07-02 21:20:14 -07:00
topjohnwu
d660401063 Treat magisk symlinks differently 2025-07-02 21:20:14 -07:00
topjohnwu
88541d6f49 Fix file attribute copy in module mounting logic
Due to various reasons, we cannot directly mount module files in /data
into the real paths. Instead we bind mount the module root directory and
remount this mirror with specific mount-point flags. Relevant to this
bug, the module mount is mounted as read-only, which means the file
attribute copy operation could fail in certain configurations.

The fix here is to always copy file attributes into writable locations,
so either in the tmpfs worker directory, or in the module directory
under /data.

A new test case is added to make sure this regression will no longer
happen again in the future.

Fix #9139
2025-07-02 19:23:46 -07:00
topjohnwu
ecd6129fe5 Add systemless hosts test 2025-07-02 19:23:46 -07:00
topjohnwu
6dfe9df9e2 Run cargo fmt 2025-07-02 19:23:46 -07:00
42 changed files with 514 additions and 284 deletions

View File

@@ -24,6 +24,7 @@ import androidx.core.widget.ImageViewCompat
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.databinding.InverseMethod
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
@@ -33,9 +34,11 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.slider.Slider
import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.widget.IndeterminateCheckBox
@@ -306,3 +309,38 @@ fun TextView.setText(text: TextHolder) {
fun Spinner.setAdapter(items: Array<Any>, layoutRes: Int) {
adapter = ArrayAdapter(context, layoutRes, items)
}
@BindingAdapter("labelFormatter")
fun Slider.setLabelFormatter(formatter: (Float) -> Int) {
setLabelFormatter { value -> resources.getString(formatter(value)) }
}
@InverseBindingAdapter(attribute = "android:value")
fun Slider.getValueBinding() = value
@BindingAdapter("android:valueAttrChanged")
fun Slider.setListener(attrChange: InverseBindingListener) {
addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) = Unit
override fun onStopTrackingTouch(slider: Slider) = attrChange.onChange()
})
}
@InverseMethod("sliderValueToPolicy")
fun policyToSliderValue(policy: Int): Float {
return when (policy) {
SuPolicy.DENY -> 1f
SuPolicy.RESTRICT -> 2f
SuPolicy.ALLOW -> 3f
else -> 1f
}
}
fun sliderValueToPolicy(value: Float): Int {
return when (value) {
1f -> SuPolicy.DENY
2f -> SuPolicy.RESTRICT
3f -> SuPolicy.ALLOW
else -> SuPolicy.DENY
}
}

View File

@@ -322,6 +322,12 @@ object Reauthenticate : BaseSettingsItem.Toggle() {
override var value by Config::suReAuth
override fun refresh() {
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Info.showSuperUser
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
}
}
object Restrict : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_restrict_title.asText()
override val description = CoreR.string.settings_su_restrict_summary.asText()
override var value by Config::suRestrict
}

View File

@@ -83,6 +83,9 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
// Can hide overlay windows on 12.0+
list.remove(Tapjack)
}
if (Const.Version.atLeast_30_1()) {
list.add(Restrict)
}
}
return list

View File

@@ -4,11 +4,13 @@ import android.graphics.drawable.Drawable
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.core.R as CoreR
class PolicyRvItem(
private val viewModel: SuperuserViewModel,
@@ -33,14 +35,34 @@ class PolicyRvItem(
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
val showSlider = Config.suRestrict || item.policy == SuPolicy.RESTRICT
@get:Bindable
var isEnabled
get() = item.policy == SuPolicy.ALLOW
get() = item.policy >= SuPolicy.ALLOW
set(value) = setImpl(value, isEnabled) {
notifyPropertyChanged(BR.enabled)
viewModel.togglePolicy(this, value)
viewModel.updatePolicy(this, if (it) SuPolicy.ALLOW else SuPolicy.DENY)
}
@get:Bindable
var sliderValue
get() = item.policy
set(value) = setImpl(value, sliderValue) {
notifyPropertyChanged(BR.sliderValue)
notifyPropertyChanged(BR.enabled)
viewModel.updatePolicy(this, it)
}
val sliderValueToPolicyString: (Float) -> Int = { value ->
when (value.toInt()) {
1 -> CoreR.string.deny
2 -> CoreR.string.restrict
3 -> CoreR.string.grant
else -> CoreR.string.deny
}
}
@get:Bindable
var shouldNotify
get() = item.notification

View File

@@ -156,15 +156,16 @@ class SuperuserViewModel(
}
}
fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun updatePolicy(item: PolicyRvItem, policy: Int) {
val items = itemsPolicies.filter { it.item.uid == item.item.uid }
fun updateState() {
viewModelScope.launch {
val res = if (enable) R.string.su_snack_grant else R.string.su_snack_deny
item.item.policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY
val res = if (policy >= SuPolicy.ALLOW) R.string.su_snack_grant else R.string.su_snack_deny
item.item.policy = policy
db.update(item.item)
items.forEach {
it.notifyPropertyChanged(BR.enabled)
it.notifyPropertyChanged(BR.sliderValue)
}
SnackbarEvent(res.asText(item.appName)).publish()
}

View File

@@ -25,7 +25,7 @@
<include
android:id="@+id/log_track_container"
bullet="@{item.log.action == 2 ? R.drawable.ic_check_md2 : R.drawable.ic_close_md2}"
bullet="@{item.log.action >= 2 ? R.drawable.ic_check_md2 : R.drawable.ic_close_md2}"
isBottom="@{item.isBottom}"
isSelected="@{item.log.action != 2}"
isTop="@{item.isTop}"

View File

@@ -5,6 +5,8 @@
<data>
<import type="com.topjohnwu.magisk.databinding.DataBindingAdaptersKt" />
<variable
name="item"
type="com.topjohnwu.magisk.ui.superuser.PolicyRvItem" />
@@ -85,16 +87,32 @@
app:layout_constraintVertical_bias="0"
tools:text="com.topjohnwu.magisk" />
<com.google.android.material.switchmaterial.SwitchMaterial
<FrameLayout
android:id="@+id/policy_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l1"
android:checked="@={item.enabled}"
android:nextFocusLeft="@id/policy"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.switchmaterial.SwitchMaterial
gone="@{item.showSlider}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={item.enabled}" />
<com.google.android.material.slider.Slider
goneUnless="@{item.showSlider}"
labelFormatter="@{item.sliderValueToPolicyString}"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:stepSize="1"
android:value="@={DataBindingAdaptersKt.policyToSliderValue(item.sliderValue)}"
android:valueFrom="1"
android:valueTo="3" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -32,6 +32,7 @@ object Config : PreferenceConfig, DBConfig {
const val SU_NOTIFICATION = "su_notification"
const val SU_REAUTH = "su_reauth"
const val SU_TAPJACK = "su_tapjack"
const val SU_RESTRICT = "su_restrict"
const val CHECK_UPDATES = "check_update"
const val RELEASE_CHANNEL = "release_channel"
const val CUSTOM_CHANNEL = "custom_channel"
@@ -147,6 +148,7 @@ object Config : PreferenceConfig, DBConfig {
}
var suReAuth by preference(Key.SU_REAUTH, false)
var suTapjack by preference(Key.SU_TAPJACK, true)
var suRestrict by preference(Key.SU_RESTRICT, false)
private const val SU_FINGERPRINT = "su_fingerprint"
private const val UPDATE_CHANNEL = "update_channel"

View File

@@ -26,9 +26,11 @@ object Const {
const val MIN_VERSION = "v22.0"
const val MIN_VERCODE = 22000
fun atLeast_24_0() = Info.env.versionCode >= 24000
fun atLeast_25_0() = Info.env.versionCode >= 25000
fun atLeast_28_0() = Info.env.versionCode >= 28000
private fun isCanary() = (Info.env.versionCode % 100) != 0
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
fun atLeast_28_0() = Info.env.versionCode >= 28000 || isCanary()
fun atLeast_30_1() = Info.env.versionCode >= 30100 || isCanary()
}
object ID {

View File

@@ -4,15 +4,16 @@ import com.topjohnwu.magisk.core.data.magiskdb.MagiskDB
class SuPolicy(
val uid: Int,
var policy: Int = INTERACTIVE,
var policy: Int = QUERY,
var remain: Long = -1L,
var logging: Boolean = true,
var notification: Boolean = true,
) {
companion object {
const val INTERACTIVE = 0
const val QUERY = 0
const val DENY = 1
const val ALLOW = 2
const val RESTRICT = 3
}
fun toMap(): MutableMap<String, Any> {

View File

@@ -70,7 +70,7 @@ object SuCallbackHandler {
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
if (notify)
notify(context, log.action == SuPolicy.ALLOW, log.appName)
notify(context, log.action >= SuPolicy.ALLOW, log.appName)
runBlocking { ServiceLocator.logRepo.insert(log) }
}
@@ -86,7 +86,7 @@ object SuCallbackHandler {
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
}.getOrNull() ?: "[UID] $uid"
notify(context, policy == SuPolicy.ALLOW, appName)
notify(context, policy >= SuPolicy.ALLOW, appName)
}
private fun notify(context: Context, granted: Boolean, appName: String) {

View File

@@ -82,7 +82,11 @@ class SuRequestHandler(
}
suspend fun respond(action: Int, time: Long) {
policy.policy = action
if (action == SuPolicy.ALLOW && Config.suRestrict) {
policy.policy = SuPolicy.RESTRICT
} else {
policy.policy = action
}
if (time >= 0) {
policy.remain = TimeUnit.MINUTES.toSeconds(time)
} else {

View File

@@ -55,7 +55,7 @@ class AdditionalTest : BaseTest {
@Test
fun testModuleCount() {
var expected = 2
var expected = 3
if (Environment.mount()) expected++
if (Environment.preinit()) expected++
if (Environment.lsposed()) expected++
@@ -90,8 +90,8 @@ class AdditionalTest : BaseTest {
assertNotNull("$MOUNT_TEST is not installed", modules.find { it.id == MOUNT_TEST })
assertTrue(
"/system/etc/newfile should exist",
RootUtils.fs.getFile("/system/etc/newfile").exists()
"/system/fonts/newfile should exist",
RootUtils.fs.getFile("/system/fonts/newfile").exists()
)
assertFalse(
"/system/bin/screenrecord should not exist",

View File

@@ -98,8 +98,8 @@ class Environment : BaseTest {
val error = "$MOUNT_TEST setup failed"
val path = root.getChildFile(MOUNT_TEST)
// Create /system/etc/newfile
val etc = path.getChildFile("system").getChildFile("etc")
// Create /system/fonts/newfile
val etc = path.getChildFile("system").getChildFile("fonts")
assertTrue(error, etc.mkdirs())
assertTrue(error, etc.getChildFile("newfile").createNewFile())
@@ -116,6 +116,10 @@ class Environment : BaseTest {
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
}
private fun setupSystemlessHost() {
assertTrue("hosts setup failed", Shell.cmd("add_hosts_module").exec().isSuccess)
}
private fun setupSepolicyRuleModule(root: ExtendedFile) {
val error = "$SEPOLICY_RULE setup failed"
val path = root.getChildFile(SEPOLICY_RULE)
@@ -215,6 +219,7 @@ class Environment : BaseTest {
val root = RootUtils.fs.getFile(Const.MODULE_PATH)
if (mount()) { setupMountTest(root) }
if (preinit()) { setupSepolicyRuleModule(root) }
setupSystemlessHost()
setupEmptyZygiskModule(root)
setupInvalidZygiskModule(root)
setupRemoveModule(root)

View File

@@ -54,6 +54,7 @@
<string name="touch_filtered_warning">由于某个应用遮挡了超级用户请求界面,因此 Magisk 无法验证您的回应</string>
<string name="deny">拒绝</string>
<string name="prompt">提示</string>
<string name="restrict">受限</string>
<string name="grant">允许</string>
<string name="su_warning">将授予对该设备的最高权限。\n如果不确定请拒绝</string>
<string name="forever">永久</string>
@@ -173,6 +174,8 @@
<string name="settings_su_auth_title">身份验证</string>
<string name="settings_su_auth_summary">对超级用户请求验证身份</string>
<string name="settings_su_auth_insecure">设备未配置验证方式</string>
<string name="settings_su_restrict_title">限制超级用户权能</string>
<string name="settings_su_restrict_summary">默认限制新的超级用户应用。警告,这会破坏大多数应用,不建议启用。</string>
<string name="settings_customization">个性化</string>
<string name="setting_add_shortcut_summary">在隐藏后难以识别名称和图标的情况下,添加快捷方式到桌面</string>
<string name="settings_doh_title">安全 DNSDoH</string>

View File

@@ -53,6 +53,7 @@
<string name="touch_filtered_warning">Because an app is obscuring a Superuser request, Magisk can\'t verify your response.</string>
<string name="deny">Deny</string>
<string name="prompt">Prompt</string>
<string name="restrict">Restrict</string>
<string name="grant">Grant</string>
<string name="su_warning">Grants full access to your device.\nDeny if you\'re not sure!</string>
<string name="forever">Forever</string>
@@ -170,6 +171,8 @@
<string name="settings_su_auth_title">User authentication</string>
<string name="settings_su_auth_summary">Ask for user authentication during Superuser requests</string>
<string name="settings_su_auth_insecure">No authentication method is configured on the device</string>
<string name="settings_su_restrict_title">Restrict root capabilities</string>
<string name="settings_su_restrict_summary">Will restrict new superuser apps by default. Warning, this will break most apps, do not enable it.</string>
<string name="settings_customization">Customization</string>
<string name="setting_add_shortcut_summary">Add a pretty shortcut to the home screen in case the name and icon are difficult to recognize after hiding the app</string>
<string name="settings_doh_title">DNS over HTTPS</string>

View File

@@ -30,4 +30,4 @@ android.nonFinalResIds=false
# Magisk
magisk.stubVersion=40
magisk.versionCode=30000
magisk.versionCode=30100

View File

@@ -16,9 +16,17 @@ The following details should ensure that modules are properly disabled:
Magisk no longer handles root hiding. There are plenty of Magisk/Zygisk modules available that specifically provide these functionalities, please search around 😉
### Q: After I hidden the Magisk app, the app icon is broken.
### Q: Magisk App shows Magisk Installed = N/A after an update but magisk su is still working.
When hiding the Magisk app, it will install a "stub" APK that has nothing in it. The only functionality this stub app has is downloading the full Magisk app APK into its internal storage and dynamically loading it. Due to the fact that the APK is literally _empty_, it does not contain the image resource for the app icon.
If upgrading with App hidden (ie. you took the 'Hide the Magisk app' option), the stub app (for hiding Magisk) may remain while a full Magisk app is also installed. This creates a conflict and the full app fails to see or access root... Uninstalling and reinstalling the full app can fix this, but not if a hidden app (stub) still exists.
The solution is to check for a hidden stub app and remove it. It may not show up normally in your launcher homescreen any longer, but should be visible from general settings, Apps. The hidden app will be named 'Settings' (default) or whatever you named it during the hiding process. Note that it is possible to have multiple obfuscated apps present. Uninstall any iterations of the hidden app you find and try opening the full app again. If necessary, uninstall it and reinstall the full app matching the binaries installed. Typing magisk -c in a terminal emulator app will show the version and version code for Magisk binaries installed (despite Installed = N/A showing).
Additionally, if a 'second space', eg. Workspace, Parallel Space etc, or another sandboxed environment, eg. a Multiple User additional profile, Island app or similar, is set up, check that no iterations of Magisk (either hidden or full apps) are running within these environments.
### Q: After I take the 'Hide the Magisk app' option the app icon is broken.
When hiding the Magisk app, it will install a "stub" APK that has nothing in it. The only functionality this stub app has is to download the full Magisk app APK data into its internal storage and dynamically load it. Due to the fact that the stub APK is literally empty, it does not contain the image resource for the app icon.
When you open the hidden Magisk app, it will offer you the option to create a shortcut in the homescreen (which has both the correct app name and icon) for your convenience. You can also manually ask the app to create the icon in app settings.

View File

@@ -583,7 +583,7 @@ impl<S: Utf8CStrBuf + Sized> FsPathBuilder for S {
}
fn append_path_fmt<T: Display>(&mut self, name: T) -> &mut Self {
self.write_fmt(format_args!("/{}", name)).ok();
self.write_fmt(format_args!("/{name}")).ok();
self
}
}
@@ -595,7 +595,7 @@ impl FsPathBuilder for dyn Utf8CStrBuf + '_ {
}
fn append_path_fmt<T: Display>(&mut self, name: T) -> &mut Self {
self.write_fmt(format_args!("/{}", name)).ok();
self.write_fmt(format_args!("/{name}")).ok();
self
}
}
@@ -846,7 +846,7 @@ fn parse_mount_info_line(line: &str) -> Option<MountInfo> {
pub fn parse_mount_info(pid: &str) -> Vec<MountInfo> {
let mut res = vec![];
let mut path = format!("/proc/{}/mountinfo", pid);
let mut path = format!("/proc/{pid}/mountinfo");
if let Ok(file) = Utf8CStr::from_string(&mut path).open(O_RDONLY | O_CLOEXEC) {
BufReader::new(file).foreach_lines(|line| {
parse_mount_info_line(line)

View File

@@ -184,6 +184,10 @@ int parse_int(string_view s) {
return parse_num<int, 10>(s);
}
uint32_t parse_uint32_hex(string_view s) {
return parse_num<uint32_t, 16>(s);
}
int switch_mnt_ns(int pid) {
int ret = -1;
int fd = syscall(__NR_pidfd_open, pid, 0);

View File

@@ -175,6 +175,7 @@ rust::Vec<size_t> mut_u8_patch(
rust::Slice<const uint8_t> from,
rust::Slice<const uint8_t> to);
uint32_t parse_uint32_hex(std::string_view s);
int parse_int(std::string_view s);
using thread_entry = void *(*)(void *);

View File

@@ -99,7 +99,7 @@ impl<T> EarlyExitExt<T> for Result<T, EarlyExit> {
exit(0)
}
Err(_) => {
eprintln!("{}", output);
eprintln!("{output}");
print_help_msg();
exit(1)
}

View File

@@ -171,7 +171,7 @@ impl<T, E: Display> Loggable<T> for Result<T, E> {
write!(w, "[{}:{}] ", caller.file(), caller.line())?;
}
f(w)?;
writeln!(w, ": {:#}", e)
writeln!(w, ": {e:#}")
});
Err(LoggedError::default())
}
@@ -366,7 +366,7 @@ impl Display for OsError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let error = self.as_io_error();
if self.name.is_empty() {
write!(f, "{:#}", error)
write!(f, "{error:#}")
} else {
match (self.arg1.ok(), self.arg2.ok()) {
(Some(arg1), Some(arg2)) => {

View File

@@ -269,13 +269,13 @@ impl Cpio {
}
fn load_from_file(path: &Utf8CStr) -> LoggedResult<Self> {
eprintln!("Loading cpio: [{}]", path);
eprintln!("Loading cpio: [{path}]");
let file = MappedFile::open(path)?;
Self::load_from_data(file.as_ref())
}
fn dump(&self, path: &str) -> LoggedResult<()> {
eprintln!("Dumping cpio: [{}]", path);
eprintln!("Dumping cpio: [{path}]");
let mut file = File::create(path)?;
let mut pos = 0usize;
let mut inode = 300000i64;
@@ -320,13 +320,13 @@ impl Cpio {
fn rm(&mut self, path: &str, recursive: bool) {
let path = norm_path(path);
if self.entries.remove(&path).is_some() {
eprintln!("Removed entry [{}]", path);
eprintln!("Removed entry [{path}]");
}
if recursive {
let path = path + "/";
self.entries.retain(|k, _| {
if k.starts_with(&path) {
eprintln!("Removed entry [{}]", k);
eprintln!("Removed entry [{k}]");
false
} else {
true
@@ -340,7 +340,7 @@ impl Cpio {
.entries
.get(path)
.ok_or_else(|| log_err!("No such file"))?;
eprintln!("Extracting entry [{}] to [{}]", path, out);
eprintln!("Extracting entry [{path}] to [{out}]");
let out = Utf8CStr::from_string(out);
@@ -435,7 +435,7 @@ impl Cpio {
data: content,
}),
);
eprintln!("Add file [{}] ({:04o})", path, mode);
eprintln!("Add file [{path}] ({mode:04o})");
Ok(())
}
@@ -451,7 +451,7 @@ impl Cpio {
data: vec![],
}),
);
eprintln!("Create directory [{}] ({:04o})", dir, mode);
eprintln!("Create directory [{dir}] ({mode:04o})");
}
fn ln(&mut self, src: &str, dst: &str) {
@@ -466,7 +466,7 @@ impl Cpio {
data: norm_path(src).as_bytes().to_vec(),
}),
);
eprintln!("Create symlink [{}] -> [{}]", dst, src);
eprintln!("Create symlink [{dst}] -> [{src}]");
}
fn mv(&mut self, from: &str, to: &str) -> LoggedResult<()> {
@@ -475,7 +475,7 @@ impl Cpio {
.remove(&norm_path(from))
.ok_or_else(|| log_err!("no such entry {}", from))?;
self.entries.insert(norm_path(to), entry);
eprintln!("Move [{}] -> [{}]", from, to);
eprintln!("Move [{from}] -> [{to}]");
Ok(())
}
@@ -498,7 +498,7 @@ impl Cpio {
if !recursive && !p.is_empty() && p.matches('/').count() > 1 {
continue;
}
println!("{}\t{}", entry, name);
println!("{entry}\t{name}");
}
}
}
@@ -511,8 +511,7 @@ impl Cpio {
let keep_verity = check_env("KEEPVERITY");
let keep_force_encrypt = check_env("KEEPFORCEENCRYPT");
eprintln!(
"Patch with flag KEEPVERITY=[{}] KEEPFORCEENCRYPT=[{}]",
keep_verity, keep_force_encrypt
"Patch with flag KEEPVERITY=[{keep_verity}] KEEPFORCEENCRYPT=[{keep_force_encrypt}]"
);
self.entries.retain(|name, entry| {
let fstab = (!keep_verity || !keep_force_encrypt)
@@ -523,7 +522,7 @@ impl Cpio {
&& name.starts_with("fstab");
if !keep_verity {
if fstab {
eprintln!("Found fstab file [{}]", name);
eprintln!("Found fstab file [{name}]");
let len = patch_verity(entry.data.as_mut_slice());
if len != entry.data.len() {
entry.data.resize(len, 0);
@@ -581,7 +580,7 @@ impl Cpio {
} else {
&name[8..]
};
eprintln!("Restore [{}] -> [{}]", name, new_name);
eprintln!("Restore [{name}] -> [{new_name}]");
backups.insert(new_name.to_string(), entry);
}
});
@@ -658,16 +657,16 @@ impl Cpio {
match action {
Action::Backup(name, mut entry) => {
let backup = if !skip_compress && entry.compress() {
format!(".backup/{}.xz", name)
format!(".backup/{name}.xz")
} else {
format!(".backup/{}", name)
format!(".backup/{name}")
};
eprintln!("Backup [{}] -> [{}]", name, backup);
eprintln!("Backup [{name}] -> [{backup}]");
backups.insert(backup, entry);
}
Action::Record(name) => {
eprintln!("Record new entry: [{}] -> [.backup/.rmlist]", name);
rm_list.push_str(&format!("{}\0", name));
eprintln!("Record new entry: [{name}] -> [.backup/.rmlist]");
rm_list.push_str(&format!("{name}\0"));
}
Action::Noop => {}
}

View File

@@ -131,9 +131,9 @@ fn print_node(node: &FdtNode) {
}
);
} else if size > MAX_PRINT_LEN {
println!("[{}]: <bytes>({})", name, size);
println!("[{name}]: <bytes>({size})");
} else {
println!("[{}]: {:02x?}", name, value);
println!("[{name}]: {value:02x?}");
}
}
@@ -154,7 +154,7 @@ fn for_each_fdt<F: FnMut(usize, Fdt) -> LoggedResult<()>>(
rw: bool,
mut f: F,
) -> LoggedResult<()> {
eprintln!("Loading dtbs from [{}]", file);
eprintln!("Loading dtbs from [{file}]");
let file = if rw {
MappedFile::open_rw(file)?
} else {
@@ -173,7 +173,7 @@ fn for_each_fdt<F: FnMut(usize, Fdt) -> LoggedResult<()>>(
}
let fdt = match Fdt::new(slice) {
Err(FdtError::BufferTooSmall) => {
eprintln!("dtb.{:04} is truncated", dtb_num);
eprintln!("dtb.{dtb_num:04} is truncated");
break;
}
Ok(fdt) => fdt,
@@ -198,11 +198,11 @@ fn dtb_print(file: &Utf8CStr, fstab: bool) -> LoggedResult<()> {
for_each_fdt(file, false, |n, fdt| {
if fstab {
if let Some(fstab) = find_fstab(&fdt) {
eprintln!("Found fstab in dtb.{:04}", n);
eprintln!("Found fstab in dtb.{n:04}");
print_node(&fstab);
}
} else if let Some(mut root) = fdt.find_node("/") {
eprintln!("Printing dtb.{:04}", n);
eprintln!("Printing dtb.{n:04}");
if root.name.is_empty() {
root.name = "/";
}
@@ -248,7 +248,7 @@ fn dtb_patch(file: &Utf8CStr) -> LoggedResult<bool> {
&mut *std::mem::transmute::<&[u8], &UnsafeCell<[u8]>>(w).get()
};
w[..=4].copy_from_slice(b"want");
eprintln!("Patch [skip_initramfs] -> [want_initramfs] in dtb.{:04}", n);
eprintln!("Patch [skip_initramfs] -> [want_initramfs] in dtb.{n:04}");
patched = true;
}
});

View File

@@ -45,7 +45,7 @@ fn remove_pattern(buf: &mut [u8], pattern_matcher: unsafe fn(&[u8]) -> Option<us
let skipped = buf.get_unchecked(read..(read + len));
// SAFETY: all matching patterns are ASCII bytes
let skipped = std::str::from_utf8_unchecked(skipped);
eprintln!("Remove pattern [{}]", skipped);
eprintln!("Remove pattern [{skipped}]");
sz -= len;
read += len;
} else {
@@ -114,7 +114,7 @@ pub fn hexpatch(file: &[u8], from: &[u8], to: &[u8]) -> bool {
let v = map.patch(pattern.as_slice(), patch.as_slice());
for off in &v {
eprintln!("Patch @ {:#010X} [{}] -> [{}]", off, from, to);
eprintln!("Patch @ {off:#010X} [{from}] -> [{to}]");
}
!v.is_empty()
};

View File

@@ -34,7 +34,7 @@ fn do_extract_boot_from_payload(
let mut reader = BufReader::new(if in_path == "-" {
unsafe { File::from_raw_fd(0) }
} else {
File::open(in_path).log_with_msg(|w| write!(w, "Cannot open '{}'", in_path))?
File::open(in_path).log_with_msg(|w| write!(w, "Cannot open '{in_path}'"))?
});
let buf = &mut [0u8; 4];
@@ -107,7 +107,7 @@ fn do_extract_boot_from_payload(
};
let mut out_file =
File::create(out_path).log_with_msg(|w| write!(w, "Cannot write to '{}'", out_path))?;
File::create(out_path).log_with_msg(|w| write!(w, "Cannot write to '{out_path}'"))?;
// Skip the manifest signature
reader.skip(manifest_sig_len as usize)?;

View File

@@ -336,7 +336,7 @@ fn switch_cgroup(cgroup: &str, pid: i32) {
}
if let Ok(mut file) = buf.open(O_WRONLY | O_APPEND | O_CLOEXEC) {
buf.clear();
buf.write_fmt(format_args!("{}", pid)).ok();
buf.write_fmt(format_args!("{pid}")).ok();
file.write_all(buf.as_bytes()).log_ok();
}
}

View File

@@ -88,6 +88,7 @@ pub mod ffi {
Query,
Deny,
Allow,
Restrict,
}
struct ModuleInfo {
@@ -117,6 +118,7 @@ pub mod ffi {
target_pid: i32,
login: bool,
keep_env: bool,
drop_cap: bool,
shell: String,
command: String,
context: String,

View File

@@ -131,7 +131,7 @@ fn write_log_to_pipe(mut logd: &File, prio: i32, msg: &Utf8CStr) -> io::Result<u
let result = logd.write_vectored(&[io1, io2]);
if let Err(ref e) = result {
let mut buf = cstr::buf::default();
buf.write_fmt(format_args!("Cannot write_log_to_pipe: {}", e))
buf.write_fmt(format_args!("Cannot write_log_to_pipe: {e}"))
.ok();
android_log_write(LogLevel::Error, &buf);
}
@@ -142,11 +142,11 @@ static MAGISK_LOGD_FD: Mutex<Option<Arc<File>>> = Mutex::new(None);
fn with_logd_fd<R, F: FnOnce(&File) -> io::Result<R>>(f: F) {
let fd = MAGISK_LOGD_FD.lock().unwrap().clone();
if let Some(logd) = fd {
if f(&logd).is_err() {
// If any error occurs, shut down the logd pipe
*MAGISK_LOGD_FD.lock().unwrap() = None;
}
if let Some(logd) = fd
&& f(&logd).is_err()
{
// If any error occurs, shut down the logd pipe
*MAGISK_LOGD_FD.lock().unwrap() = None;
}
}

View File

@@ -1,4 +1,4 @@
use crate::consts::{MODULEMNT, WORKERDIR};
use crate::consts::{MODULEMNT, MODULEROOT, WORKERDIR};
use crate::ffi::{ModuleInfo, get_magisk_tmp};
use crate::load_prop_file;
use base::{
@@ -9,8 +9,12 @@ use libc::{MS_RDONLY, O_CLOEXEC, O_CREAT, O_RDONLY};
use std::collections::BTreeMap;
use std::path::{Component, Path};
const MAGISK_BIN_INJECT_PARTITIONS: [&Utf8CStr; 4] =
[cstr!("/system/"), cstr!("/vendor/"), cstr!("/product/"), cstr!("/system_ext/")];
const MAGISK_BIN_INJECT_PARTITIONS: [&Utf8CStr; 4] = [
cstr!("/system/"),
cstr!("/vendor/"),
cstr!("/product/"),
cstr!("/system_ext/"),
];
const SECONDARY_READ_ONLY_PARTITIONS: [&Utf8CStr; 3] =
[cstr!("/vendor"), cstr!("/product"), cstr!("/system_ext")];
@@ -40,62 +44,93 @@ fn mount_dummy(reason: &str, src: &Utf8CStr, dest: &Utf8CStr, is_dir: bool) -> O
bind_mount(reason, src, dest, false)
}
// File paths that act like a stack, popping out the last element
// File path that act like a stack, popping out the last element
// automatically when out of scope. Using Rust's lifetime mechanism,
// we can ensure the buffer will never be incorrectly copied or modified.
// After calling append or clone, the mutable reference's lifetime is
// After calling append or reborrow, the mutable reference's lifetime is
// "transferred" to the returned object, and the compiler will guarantee
// that the original mutable reference can only be reused if and only if
// the newly created instance is destroyed.
struct PathTracker<'a> {
real: &'a mut dyn Utf8CStrBuf,
tmp: &'a mut dyn Utf8CStrBuf,
real_len: usize,
tmp_len: usize,
path: &'a mut dyn Utf8CStrBuf,
len: usize,
}
impl PathTracker<'_> {
fn from<'a>(real: &'a mut dyn Utf8CStrBuf, tmp: &'a mut dyn Utf8CStrBuf) -> PathTracker<'a> {
let real_len = real.len();
let tmp_len = tmp.len();
PathTracker {
real,
tmp,
real_len,
tmp_len,
}
fn from<'a>(path: &'a mut dyn Utf8CStrBuf) -> PathTracker<'a> {
let len = path.len();
PathTracker { path, len }
}
fn append(&mut self, name: &str) -> PathTracker {
let real_len = self.real.len();
let tmp_len = self.tmp.len();
self.real.append_path(name);
self.tmp.append_path(name);
let len = self.path.len();
self.path.append_path(name);
PathTracker {
real: self.real,
tmp: self.tmp,
real_len,
tmp_len,
path: self.path,
len,
}
}
fn clone(&mut self) -> PathTracker {
Self::from(self.real, self.tmp)
fn reborrow(&mut self) -> PathTracker {
Self::from(self.path)
}
}
impl Drop for PathTracker<'_> {
// Revert back to the original state after finish using the buffer
fn drop(&mut self) {
self.real.truncate(self.real_len);
self.tmp.truncate(self.tmp_len);
self.path.truncate(self.len);
}
}
struct FilePaths<'a> {
real: PathTracker<'a>,
worker: PathTracker<'a>,
module_mnt: PathTracker<'a>,
module_root: PathTracker<'a>,
}
impl FilePaths<'_> {
fn append(&mut self, name: &str) -> FilePaths {
FilePaths {
real: self.real.append(name),
worker: self.worker.append(name),
module_mnt: self.module_mnt.append(name),
module_root: self.module_root.append(name),
}
}
fn reborrow(&mut self) -> FilePaths {
FilePaths {
real: self.real.reborrow(),
worker: self.worker.reborrow(),
module_mnt: self.module_mnt.reborrow(),
module_root: self.module_root.reborrow(),
}
}
fn real(&self) -> &Utf8CStr {
self.real.path
}
fn worker(&self) -> &Utf8CStr {
self.worker.path
}
fn module_mnt(&self) -> &Utf8CStr {
self.module_mnt.path
}
fn module(&self) -> &Utf8CStr {
self.module_root.path
}
}
enum FsNode {
Directory { children: FsNodeMap },
File { src: Utf8CString },
Symlink { target: Utf8CString, is_magisk_bin: bool },
Symlink { target: Utf8CString },
MagiskLink,
Whiteout,
}
@@ -106,21 +141,20 @@ impl FsNode {
}
}
fn build_from_path(&mut self, path: &mut dyn Utf8CStrBuf) -> LoggedResult<()> {
fn collect(&mut self, mut paths: FilePaths) -> LoggedResult<()> {
let FsNode::Directory { children } = self else {
return Ok(());
};
let mut dir = Directory::open(path)?;
let path_len = path.len();
let mut dir = Directory::open(paths.module())?;
while let Some(entry) = dir.read()? {
path.truncate(path_len);
path.append_path(entry.name());
let entry_paths = paths.append(entry.name());
let path = entry_paths.module();
if entry.is_dir() {
let node = children
.entry(entry.name().to_string())
.or_insert_with(FsNode::new_dir);
node.build_from_path(path)?;
node.collect(entry_paths)?;
} else if entry.is_symlink() {
let mut link = cstr::buf::default();
path.read_link(&mut link)?;
@@ -128,7 +162,6 @@ impl FsNode {
.entry(entry.name().to_string())
.or_insert_with(|| FsNode::Symlink {
target: link.to_owned(),
is_magisk_bin: false,
});
} else {
if entry.is_char_device() {
@@ -140,10 +173,14 @@ impl FsNode {
continue;
}
}
if entry_paths.real().exists() {
clone_attr(entry_paths.real(), path)?;
}
children
.entry(entry.name().to_string())
.or_insert_with(|| FsNode::File {
src: path.to_owned(),
// Make sure to mount from module_mnt, not module
src: entry_paths.module_mnt().to_owned(),
});
}
}
@@ -164,7 +201,7 @@ impl FsNode {
true
}
}
FsNode::Symlink { .. } | FsNode::Whiteout => true,
_ => true,
}
}
@@ -175,7 +212,7 @@ impl FsNode {
}
}
fn commit(&mut self, mut path: PathTracker, is_root_dir: bool) -> LoggedResult<()> {
fn commit(&mut self, mut path: FilePaths, is_root_dir: bool) -> LoggedResult<()> {
match self {
FsNode::Directory { children } => {
let mut is_tmpfs = false;
@@ -184,7 +221,7 @@ impl FsNode {
children.retain(|name, node| {
if name == ".replace" {
return if is_root_dir {
warn!("Unable to replace '{}', ignore request", path.real);
warn!("Unable to replace '{}', ignore request", path.real());
false
} else {
is_tmpfs = true;
@@ -193,10 +230,10 @@ impl FsNode {
}
let path = path.append(name);
if node.parent_should_be_tmpfs(path.real) {
if node.parent_should_be_tmpfs(path.real()) {
if is_root_dir {
// Ignore the unsupported child node
warn!("Unable to add '{}', skipped", path.real);
warn!("Unable to add '{}', skipped", path.real());
return false;
}
is_tmpfs = true;
@@ -205,10 +242,10 @@ impl FsNode {
});
if is_tmpfs {
self.commit_tmpfs(path.clone())?;
self.commit_tmpfs(path.reborrow())?;
// Transitioning from non-tmpfs to tmpfs, we need to actually mount the
// worker dir to dest after all children are committed.
bind_mount("move", path.tmp, path.real, true)?;
bind_mount("move", path.worker(), path.real(), true)?;
} else {
for (name, node) in children {
let path = path.append(name);
@@ -217,26 +254,25 @@ impl FsNode {
}
}
FsNode::File { src } => {
clone_attr(path.real, src)?;
bind_mount("mount", src, path.real, false)?;
bind_mount("mount", src, path.real(), false)?;
}
FsNode::Symlink { .. } | FsNode::Whiteout => {
error!("Unable to handle '{}': parent should be tmpfs", path.real);
_ => {
error!("Unable to handle '{}': parent should be tmpfs", path.real());
}
}
Ok(())
}
fn commit_tmpfs(&mut self, mut path: PathTracker) -> LoggedResult<()> {
fn commit_tmpfs(&mut self, mut path: FilePaths) -> LoggedResult<()> {
match self {
FsNode::Directory { children } => {
path.tmp.mkdirs(0o000)?;
if path.real.exists() {
clone_attr(path.real, path.tmp)?;
} else if let Some(p) = path.tmp.parent_dir() {
path.worker().mkdirs(0o000)?;
if path.real().exists() {
clone_attr(path.real(), path.worker())?;
} else if let Some(p) = path.worker().parent_dir() {
let parent = Utf8CString::from(p);
clone_attr(&parent, path.tmp)?;
clone_attr(&parent, path.worker())?;
}
// Check whether a file name '.replace' exist
@@ -251,7 +287,7 @@ impl FsNode {
bind_mount(
"mount",
&src,
path.real,
path.real(),
matches!(node, FsNode::Directory { .. }),
)?;
}
@@ -264,7 +300,7 @@ impl FsNode {
}
// Traverse the real directory and mount mirrors
if let Ok(mut dir) = Directory::open(path.real) {
if let Ok(mut dir) = Directory::open(path.real()) {
while let Ok(Some(entry)) = dir.read() {
if children.contains_key(entry.name().as_str()) {
// Should not be mirrored, next
@@ -281,11 +317,10 @@ impl FsNode {
entry.name().to_string(),
FsNode::Symlink {
target: link.to_owned(),
is_magisk_bin: false,
},
);
} else {
mount_dummy("mirror", path.real, path.tmp, entry.is_dir())?;
mount_dummy("mirror", path.real(), path.worker(), entry.is_dir())?;
}
}
}
@@ -297,21 +332,28 @@ impl FsNode {
}
}
FsNode::File { src } => {
if path.real.exists() {
clone_attr(path.real, src)?;
}
mount_dummy("mount", src, path.tmp, false)?;
mount_dummy("mount", src, path.worker(), false)?;
}
FsNode::Symlink { target, is_magisk_bin } => {
module_log!("mklink", path.tmp, target);
path.tmp.create_symlink_to(target)?;
// Avoid cloneing existing su attributes to our su
if !*is_magisk_bin && path.real.exists() {
clone_attr(path.real, path.tmp)?;
FsNode::Symlink { target } => {
module_log!("mklink", path.worker(), target);
path.worker().create_symlink_to(target)?;
if path.real().exists() {
clone_attr(path.real(), path.worker())?;
}
}
FsNode::MagiskLink => {
if let Some(name) = path.real().file_name()
&& name == "supolicy"
{
module_log!("mklink", path.worker(), "./magiskpolicy");
path.worker().create_symlink_to(cstr!("./magiskpolicy"))?;
} else {
module_log!("mklink", path.worker(), "./magisk");
path.worker().create_symlink_to(cstr!("./magisk"))?;
}
}
FsNode::Whiteout => {
module_log!("delete", path.real, "null");
module_log!("delete", path.real(), "null");
}
}
Ok(())
@@ -349,34 +391,15 @@ fn inject_magisk_bins(system: &mut FsNode) {
);
// Inject applet symlinks
children.insert(
"su".to_string(),
FsNode::Symlink {
target: Utf8CString::from("./magisk"),
is_magisk_bin: true,
},
);
children.insert(
"resetprop".to_string(),
FsNode::Symlink {
target: Utf8CString::from("./magisk"),
is_magisk_bin: true,
},
);
children.insert(
"supolicy".to_string(),
FsNode::Symlink {
target: Utf8CString::from("./magiskpolicy"),
is_magisk_bin: true,
},
);
children.insert("su".to_string(), FsNode::MagiskLink);
children.insert("resetprop".to_string(), FsNode::MagiskLink);
children.insert("supolicy".to_string(), FsNode::MagiskLink);
}
// Strip /system prefix to insert correct node
fn strip_system_prefix(orig_item: &str) -> String {
match orig_item.strip_prefix("/system/") {
Some(rest) => format!("/{}", rest),
Some(rest) => format!("/{rest}"),
None => orig_item.to_string(),
}
}
@@ -386,7 +409,10 @@ fn inject_magisk_bins(system: &mut FsNode) {
for orig_item in path_env.split(':') {
// Filter not suitbale paths
if !MAGISK_BIN_INJECT_PARTITIONS.iter().any(|p| orig_item.starts_with(p.as_str())) {
if !MAGISK_BIN_INJECT_PARTITIONS
.iter()
.any(|p| orig_item.starts_with(p.as_str()))
{
continue;
}
// Flatten apex path is not suitable too
@@ -395,7 +421,7 @@ fn inject_magisk_bins(system: &mut FsNode) {
}
// Override existing su first
let su_path = Utf8CString::from(format!("{}/su", orig_item));
let su_path = Utf8CString::from(format!("{orig_item}/su"));
if su_path.exists() {
let item = strip_system_prefix(orig_item);
candidates.push((item, 0));
@@ -403,21 +429,25 @@ fn inject_magisk_bins(system: &mut FsNode) {
}
let path = Utf8CString::from(orig_item);
if let Ok(attr) = path.get_attr() && (attr.st.st_mode & 0x0001) != 0 {
if let Ok(mut dir) = Directory::open(&path) {
let mut count = 0;
if let Err(_) = dir.pre_order_walk(|e| {
if let Ok(attr) = path.get_attr()
&& (attr.st.st_mode & 0x0001) != 0
&& let Ok(mut dir) = Directory::open(&path)
{
let mut count = 0;
if dir
.pre_order_walk(|e| {
if e.is_file() {
count += 1;
}
Ok(WalkResult::Continue)
}) {
// Skip, we cannot ensure the result is correct
continue;
}
let item = strip_system_prefix(orig_item);
candidates.push((item, count));
})
.is_err()
{
// Skip, we cannot ensure the result is correct
continue;
}
let item = strip_system_prefix(orig_item);
candidates.push((item, count));
}
}
@@ -516,40 +546,56 @@ fn inject_zygisk_bins(system: &mut FsNode, name: &str) {
pub fn load_modules(module_list: &[ModuleInfo], zygisk_name: &str) {
let mut system = FsNode::new_dir();
// Build all the base "prefix" paths
let mut root = cstr::buf::default().join_path("/");
let mut module_dir = cstr::buf::default().join_path(MODULEROOT);
let mut module_mnt = cstr::buf::default()
.join_path(get_magisk_tmp())
.join_path(MODULEMNT);
let mut worker = cstr::buf::default()
.join_path(get_magisk_tmp())
.join_path(WORKERDIR);
// Create a collection of all relevant paths
let mut root_paths = FilePaths {
real: PathTracker::from(&mut root),
worker: PathTracker::from(&mut worker),
module_mnt: PathTracker::from(&mut module_mnt),
module_root: PathTracker::from(&mut module_dir),
};
// Step 1: Create virtual filesystem tree
//
// In this step, there is zero logic applied during tree construction; we simply collect
// and record the union of all module filesystem trees under each of their /system directory.
let mut path = cstr::buf::default()
.join_path(get_magisk_tmp())
.join_path(MODULEMNT);
let len = path.len();
for info in module_list {
path.truncate(len);
path.append_path(&info.name);
// Read props
let module_path_len = path.len();
path.append_path("system.prop");
if path.exists() {
// Do NOT go through property service as it could cause boot lock
load_prop_file(&path, true);
let mut module_paths = root_paths.append(&info.name);
{
// Read props
let prop = module_paths.append("system.prop");
if prop.module().exists() {
// Do NOT go through property service as it could cause boot lock
load_prop_file(prop.module(), true);
}
}
// Check whether skip mounting
path.truncate(module_path_len);
path.append_path("skip_mount");
if path.exists() {
continue;
{
// Check whether skip mounting
let skip = module_paths.append("skip_mount");
if skip.module().exists() {
continue;
}
}
// Double check whether the system folder exists
path.truncate(module_path_len);
path.append_path("system");
if path.exists() {
info!("{}: loading module files", &info.name);
system.build_from_path(&mut path).log_ok();
{
// Double check whether the system folder exists
let sys = module_paths.append("system");
if sys.module().exists() {
info!("{}: loading module files", &info.name);
system.collect(sys).log_ok();
}
}
}
@@ -592,16 +638,6 @@ pub fn load_modules(module_list: &[ModuleInfo], zygisk_name: &str) {
}
roots.insert("system", system);
// Reuse the path buffer
path.clear();
path.push_str("/");
// Build the base worker directory path
let mut tmp = cstr::buf::default()
.join_path(get_magisk_tmp())
.join_path(WORKERDIR);
let mut tracker = PathTracker::from(&mut path, &mut tmp);
for (dir, mut root) in roots {
// Step 4: Convert virtual filesystem tree into concrete operations
//
@@ -610,7 +646,7 @@ pub fn load_modules(module_list: &[ModuleInfo], zygisk_name: &str) {
// The "core" of the logic is to decide which directories need to be rebuilt in the
// tmpfs worker directory, and real sub-nodes need to be mirrored inside it.
let path = tracker.append(dir);
let path = root_paths.append(dir);
root.commit(path, true).log_ok();
}
}

View File

@@ -22,37 +22,37 @@ pub fn setup_preinit_dir() {
let dev_path = cstr::buf::new::<64>()
.join_path(magisk_tmp)
.join_path(PREINITDEV);
if let Ok(attr) = dev_path.get_attr() {
if attr.st.st_mode & libc::S_IFMT as c_uint == libc::S_IFBLK.as_() {
// DO NOT mount the block device directly, as we do not know the flags and configs
// to properly mount the partition; mounting block devices directly as rw could cause
// crashes if the filesystem driver is crap (e.g. some broken F2FS drivers).
// What we do instead is to scan through the current mountinfo and find a pre-existing
// mount point mounting our desired partition, and then bind mount the target folder.
let preinit_dev = attr.st.st_rdev;
let mnt_path = cstr::buf::default()
.join_path(magisk_tmp)
.join_path(PREINITMIRR);
for info in parse_mount_info("self") {
if info.root == "/" && info.device == preinit_dev {
if !info.fs_option.split(',').any(|s| s == "rw") {
// Only care about rw mounts
continue;
}
let mut target = info.target;
let target = Utf8CStr::from_string(&mut target);
let mut preinit_dir = resolve_preinit_dir(target);
let preinit_dir = Utf8CStr::from_string(&mut preinit_dir);
let r: LoggedResult<()> = try {
preinit_dir.mkdir(0o700)?;
mnt_path.mkdirs(0o755)?;
mnt_path.remove().ok();
mnt_path.create_symlink_to(preinit_dir)?;
};
if r.is_ok() {
info!("* Found preinit dir: {}", preinit_dir);
return;
}
if let Ok(attr) = dev_path.get_attr()
&& attr.st.st_mode & libc::S_IFMT as c_uint == libc::S_IFBLK.as_()
{
// DO NOT mount the block device directly, as we do not know the flags and configs
// to properly mount the partition; mounting block devices directly as rw could cause
// crashes if the filesystem driver is crap (e.g. some broken F2FS drivers).
// What we do instead is to scan through the current mountinfo and find a pre-existing
// mount point mounting our desired partition, and then bind mount the target folder.
let preinit_dev = attr.st.st_rdev;
let mnt_path = cstr::buf::default()
.join_path(magisk_tmp)
.join_path(PREINITMIRR);
for info in parse_mount_info("self") {
if info.root == "/" && info.device == preinit_dev {
if !info.fs_option.split(',').any(|s| s == "rw") {
// Only care about rw mounts
continue;
}
let mut target = info.target;
let target = Utf8CStr::from_string(&mut target);
let mut preinit_dir = resolve_preinit_dir(target);
let preinit_dir = Utf8CStr::from_string(&mut preinit_dir);
let r: LoggedResult<()> = try {
preinit_dir.mkdir(0o700)?;
mnt_path.mkdirs(0o755)?;
mnt_path.remove().ok();
mnt_path.create_symlink_to(preinit_dir)?;
};
if r.is_ok() {
info!("* Found preinit dir: {}", preinit_dir);
return;
}
}
}
@@ -238,10 +238,10 @@ pub fn revert_unmount(pid: i32) {
let mut prev: Option<PathBuf> = None;
targets.sort();
targets.retain(|target| {
if let Some(prev) = &prev {
if Path::new(target).starts_with(prev) {
return false;
}
if let Some(prev) = &prev
&& Path::new(target).starts_with(prev)
{
return false;
}
prev = Some(PathBuf::from(target.clone()));
true

View File

@@ -167,10 +167,10 @@ pub fn persist_get_props(mut prop_cb: Pin<&mut PropCb>) {
} else {
let mut dir = Directory::open(cstr!(PERSIST_PROP_DIR))?;
dir.pre_order_walk(|e| {
if e.is_file() {
if let Ok(mut value) = file_get_prop(e.name()) {
prop_cb.exec(e.name(), Utf8CStr::from_string(&mut value));
}
if e.is_file()
&& let Ok(mut value) = file_get_prop(e.name())
{
prop_cb.exec(e.name(), Utf8CStr::from_string(&mut value));
}
// Do not traverse recursively
Ok(WalkResult::Skip)

View File

@@ -1,4 +1,4 @@
use crate::consts::{DATABIN, LOG_PIPE, MAGISK_LOG_CON, MODULEROOT, SECURE_DIR};
use crate::consts::{DATABIN, LOG_PIPE, MAGISK_LOG_CON, MAGISKDB, MODULEROOT, SECURE_DIR};
use crate::ffi::get_magisk_tmp;
use base::libc::{O_CLOEXEC, O_WRONLY};
use base::{Directory, FsPathBuilder, LoggedResult, ResultExt, Utf8CStr, Utf8CStrBuf, cstr, libc};
@@ -55,10 +55,9 @@ pub(crate) fn restorecon() {
if let Ok(mut file) = cstr!("/sys/fs/selinux/context")
.open(O_WRONLY | O_CLOEXEC)
.log()
&& file.write_all(ADB_CON.as_bytes_with_nul()).is_ok()
{
if file.write_all(ADB_CON.as_bytes_with_nul()).is_ok() {
cstr!(SECURE_DIR).set_secontext(ADB_CON).log_ok();
}
cstr!(SECURE_DIR).set_secontext(ADB_CON).log_ok();
}
let mut path = cstr::buf::default();
@@ -70,6 +69,7 @@ pub(crate) fn restorecon() {
path.clear();
path.push_str(DATABIN);
restore_syscon(&mut path).log_ok();
unsafe { libc::chmod(cstr!(MAGISKDB).as_ptr(), 0o000) };
}
pub(crate) fn restore_tmpcon() -> LoggedResult<()> {

View File

@@ -22,6 +22,7 @@ impl Default for SuRequest {
target_pid: -1,
login: false,
keep_env: false,
drop_cap: false,
shell: DEFAULT_SHELL.to_string(),
command: "".to_string(),
context: "".to_string(),
@@ -168,7 +169,10 @@ impl MagiskD {
// Before unlocking, refresh the timestamp
access.refresh();
// Fail fast
if access.settings.policy == SuPolicy::Restrict {
req.drop_cap = true;
}
if access.settings.policy == SuPolicy::Deny {
warn!("su: request rejected ({})", info.uid);
client.write_pod(&SuPolicy::Deny.repr).ok();

View File

@@ -52,13 +52,12 @@ fn set_stdin_raw() -> bool {
pub fn restore_stdin() -> bool {
unsafe {
if let Some(ref termios) = OLD_STDIN {
if tcsetattr(STDIN_FILENO, TCSAFLUSH, termios) < 0
&& tcsetattr(STDIN_FILENO, TCSADRAIN, termios) < 0
{
warn!("Failed to restore terminal attributes");
return false;
}
if let Some(ref termios) = OLD_STDIN
&& tcsetattr(STDIN_FILENO, TCSAFLUSH, termios) < 0
&& tcsetattr(STDIN_FILENO, TCSADRAIN, termios) < 0
{
warn!("Failed to restore terminal attributes");
return false;
}
OLD_STDIN = None;
true

View File

@@ -9,6 +9,9 @@
#include <getopt.h>
#include <fcntl.h>
#include <pwd.h>
#include <linux/securebits.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/stat.h>
@@ -46,6 +49,7 @@ int quit_signals[] = { SIGALRM, SIGABRT, SIGHUP, SIGPIPE, SIGQUIT, SIGTERM, SIGI
" as a primary group if the option -g is not specified\n"
" -Z, --context CONTEXT Change SELinux context\n"
" -t, --target PID PID to take mount namespace from\n"
" -d, --drop-cap Drop all Linux capabilities\n"
" -h, --help Display this help message and exit\n"
" -, -l, --login Pretend the shell to be a login shell\n"
" -m, -p,\n"
@@ -105,6 +109,7 @@ int su_client_main(int argc, char *argv[]) {
{ "group", required_argument, nullptr, 'g' },
{ "supp-group", required_argument, nullptr, 'G' },
{ "interactive", no_argument, nullptr, 'i' },
{ "drop-cap", no_argument, nullptr, 'd' },
{ nullptr, 0, nullptr, 0 },
};
@@ -121,7 +126,7 @@ int su_client_main(int argc, char *argv[]) {
bool interactive = false;
while ((c = getopt_long(argc, argv, "c:hlimps:VvuZ:Mt:g:G:", long_opts, nullptr)) != -1) {
while ((c = getopt_long(argc, argv, "c:hlimpds:VvuZ:Mt:g:G:", long_opts, nullptr)) != -1) {
switch (c) {
case 'c': {
string command;
@@ -146,6 +151,9 @@ int su_client_main(int argc, char *argv[]) {
case 'p':
req.keep_env = true;
break;
case 'd':
req.drop_cap = true;
break;
case 's':
req.shell = optarg;
break;
@@ -259,11 +267,66 @@ int su_client_main(int argc, char *argv[]) {
return code;
}
// Set effective uid back to root, otherwise setres[ug]id will fail if uid isn't root
static void set_identity(int uid, const rust::Vec<gid_t> &groups) {
if (seteuid(0)) {
PLOGE("seteuid (root)");
static void drop_caps() {
static auto last_valid_cap = []() {
uint32_t cap = CAP_WAKE_ALARM;
while (prctl(PR_CAPBSET_READ, cap) >= 0) {
cap++;
}
return cap - 1;
}();
// Drop bounding set
for (uint32_t cap = 0; cap <= last_valid_cap; cap++) {
if (cap != CAP_SETUID) {
prctl(PR_CAPBSET_DROP, cap);
}
}
// Clean inheritable set
__user_cap_header_struct header = {.version = _LINUX_CAPABILITY_VERSION_3};
__user_cap_data_struct data[_LINUX_CAPABILITY_U32S_3] = {};
if (capget(&header, &data[0]) == 0) {
for (size_t i = 0; i < _LINUX_CAPABILITY_U32S_3; i++) {
data[i].inheritable = 0;
}
capset(&header, &data[0]);
}
// All capabilities will be lost after exec
prctl(PR_SET_SECUREBITS, SECBIT_NOROOT);
// Except CAP_SETUID in bounding set, it is a marker for restricted process
}
static bool proc_is_restricted(pid_t pid) {
char buf[32] = {};
auto bnd = "CapBnd:"sv;
uint32_t data[_LINUX_CAPABILITY_U32S_3] = {};
ssprintf(buf, sizeof(buf), "/proc/%d/status", pid);
file_readline(buf, [&](string_view line) -> bool {
if (line.starts_with(bnd)) {
auto p = line.begin();
advance(p, bnd.size());
while (isspace(*p)) advance(p, 1);
line.remove_prefix(distance(line.begin(), p));
for (int i = 0; i < _LINUX_CAPABILITY_U32S_3; i++) {
auto cap = line.substr((_LINUX_CAPABILITY_U32S_3 - 1 - i) * 8, 8);
data[i] = parse_uint32_hex(cap);
}
return false;
}
return true;
});
bool equal = true;
for (int i = 0; i < _LINUX_CAPABILITY_U32S_3; i++) {
if (i == CAP_TO_INDEX(CAP_SETUID)) {
if (data[i] != CAP_TO_MASK(CAP_SETUID)) equal = false;
} else {
if (data[i] != 0) equal = false;
}
}
return equal;
}
static void set_identity(int uid, const rust::Vec<gid_t> &groups) {
gid_t gid;
if (!groups.empty()) {
if (setgroups(groups.size(), groups.data())) {
@@ -404,15 +467,21 @@ void exec_root_shell(int client, int pid, SuRequest &req, MntNsMode mode) {
}
}
// Unblock all signals
sigset_t block_set;
sigemptyset(&block_set);
sigprocmask(SIG_SETMASK, &block_set, nullptr);
// Config privileges
if (!req.context.empty()) {
auto f = xopen_file("/proc/self/attr/exec", "we");
if (f) fprintf(f.get(), "%s", req.context.c_str());
}
set_identity(req.target_uid, req.gids);
if (req.target_uid != AID_ROOT || req.drop_cap || proc_is_restricted(pid))
drop_caps();
if (req.target_uid != AID_ROOT || req.gids.size() > 0)
set_identity(req.target_uid, req.gids);
// Unblock all signals
sigset_t block_set;
sigemptyset(&block_set);
sigprocmask(SIG_SETMASK, &block_set, nullptr);
execvp(req.shell.c_str(), (char **) argv);
fprintf(stderr, "Cannot execute %s: %s\n", req.shell.c_str(), strerror(errno));
PLOGE("exec");

View File

@@ -170,10 +170,10 @@ impl MagiskD {
client.write_pod(&flags)?;
// Next send modules
if zygisk_should_load_module(flags) {
if let Some(module_fds) = self.get_module_fds(is_64_bit) {
client.send_fds(&module_fds)?;
}
if zygisk_should_load_module(flags)
&& let Some(module_fds) = self.get_module_fds(is_64_bit)
{
client.send_fds(&module_fds)?;
}
// If we're not in system_server, we are done

View File

@@ -17,7 +17,7 @@ impl<T, E: Display> ResultExt<T> for Result<T, E> {
match self {
Ok(r) => r,
Err(e) => {
eprintln!("error occurred: {}", e);
eprintln!("error occurred: {e}");
process::exit(1);
}
}
@@ -40,6 +40,6 @@ pub fn gen_cxx_binding(name: &str) {
println!("cargo:rerun-if-changed=lib.rs");
let opt = Opt::default();
let code = cxx_gen::generate_header_and_cc_with_path("lib.rs", &opt);
write_if_diff(format!("{}.cpp", name), code.implementation.as_slice()).ok_or_exit();
write_if_diff(format!("{}.hpp", name), code.header.as_slice()).ok_or_exit();
write_if_diff(format!("{name}.cpp"), code.implementation.as_slice()).ok_or_exit();
write_if_diff(format!("{name}.hpp"), code.header.as_slice()).ok_or_exit();
}

View File

@@ -16,6 +16,7 @@ pub const LOGFILE: &str = "/cache/magisk.log";
pub const SECURE_DIR: &str = "/data/adb";
pub const MODULEROOT: &str = concatcp!(SECURE_DIR, "/modules");
pub const DATABIN: &str = concatcp!(SECURE_DIR, "/magisk");
pub const MAGISKDB: &str = concatcp!(SECURE_DIR, "/magisk.db");
// tmpfs paths
const INTERNAL_DIR: &str = ".magisk";

View File

@@ -42,7 +42,7 @@ fn print_usage(cmd: &str) {
eprintln!(
r#"MagiskPolicy - SELinux Policy Patch Tool
Usage: {} [--options...] [policy statements...]
Usage: {cmd} [--options...] [policy statements...]
Options:
--help show help message for policy statements
@@ -60,8 +60,7 @@ Options:
If neither --load, --load-split, nor --compile-split is specified,
it will load from current live policies (/sys/fs/selinux/policy)
"#,
cmd
"#
);
format_statement_help(&mut FmtAdaptor(&mut stderr())).ok();

View File

@@ -504,7 +504,7 @@ impl Display for Token<'_> {
Token::ST => f.write_char('*'),
Token::TL => f.write_char('~'),
Token::HP => f.write_char('-'),
Token::HX(n) => f.write_fmt(format_args!("{:06X}", n)),
Token::HX(n) => f.write_fmt(format_args!("{n:06X}")),
Token::ID(s) => f.write_str(s),
}
}