diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift
index 9860cfd3..31d53d07 100644
--- a/AltStore/My Apps/MyAppsViewController.swift
+++ b/AltStore/My Apps/MyAppsViewController.swift
@@ -959,7 +959,7 @@ private extension MyAppsViewController
@objc func presentInactiveAppsAlert()
{
- let message: String
+ var message: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
@@ -968,6 +968,12 @@ private extension MyAppsViewController
else
{
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "")
+
+ if UserDefaults.standard.ignoreActiveAppsLimit
+ {
+ message += "\n\n"
+ message += NSLocalizedString("If you're using the MacDirtyCow exploit to remove the 3-app limit, you can install up to 10 apps and app extensions instead.", comment: "")
+ }
}
let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: message, preferredStyle: .alert)
diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard
index abc0abf6..4f391ab2 100644
--- a/AltStore/Settings/Settings.storyboard
+++ b/AltStore/Settings/Settings.storyboard
@@ -590,6 +590,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -844,6 +884,7 @@
+
diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift
index a3fd79fc..f8ffe4f4 100644
--- a/AltStore/Settings/SettingsViewController.swift
+++ b/AltStore/Settings/SettingsViewController.swift
@@ -26,6 +26,7 @@ extension SettingsViewController
case instructions
case techyThings
case credits
+ case macDirtyCow
case debug
}
@@ -88,6 +89,7 @@ final class SettingsViewController: UITableViewController
@IBOutlet private var disableAppLimitSwitch: UISwitch!
@IBOutlet private var refreshSideJITServer: UILabel!
+ @IBOutlet private var enforceThreeAppLimitSwitch: UISwitch!
@IBOutlet private var versionLabel: UILabel!
@@ -198,6 +200,7 @@ private extension SettingsViewController
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
+ self.enforceThreeAppLimitSwitch.isOn = !UserDefaults.standard.ignoreActiveAppsLimit
if self.isViewLoaded
{
@@ -261,6 +264,16 @@ private extension SettingsViewController
case .credits:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
+ case .macDirtyCow:
+ if isHeader
+ {
+ settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("MACDIRTYCOW", comment: "")
+ }
+ else
+ {
+ settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("If you've removed the 3-sideloaded app limit via the MacDirtyCow exploit, disable this setting to sideload more than 3 apps at a time.", comment: "")
+ }
+
case .debug:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "")
}
@@ -277,6 +290,20 @@ private extension SettingsViewController
let size = settingsHeaderFooterView.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
return size.height
}
+
+ func isSectionHidden(_ section: Section) -> Bool
+ {
+ switch section
+ {
+ case .macDirtyCow:
+ let ios16_2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0)
+
+ let isMacDirtyCowExploitSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2)
+ return !(isMacDirtyCowExploitSupported && UserDefaults.standard.isDebugModeEnabled)
+
+ default: return false
+ }
+ }
}
private extension SettingsViewController
@@ -342,6 +369,16 @@ private extension SettingsViewController
UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn
}
+ @IBAction func toggleEnforceThreeAppLimit(_ sender: UISwitch)
+ {
+ UserDefaults.standard.ignoreActiveAppsLimit = !sender.isOn
+
+ if UserDefaults.standard.activeAppsLimit != nil
+ {
+ UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
+ }
+ }
+
@available(iOS 14, *)
@IBAction func addRefreshAppsShortcut()
{
@@ -470,6 +507,7 @@ extension SettingsViewController
let section = Section.allCases[section]
switch section
{
+ case _ where isSectionHidden(section): return 0
case .signIn: return (self.activeTeam == nil) ? 1 : 0
case .account: return (self.activeTeam == nil) ? 0 : 3
case .appRefresh: return AppRefreshRow.allCases.count
@@ -516,9 +554,10 @@ extension SettingsViewController
let section = Section.allCases[section]
switch section
{
+ case _ where isSectionHidden(section): return nil
case .signIn where self.activeTeam != nil: return nil
case .account where self.activeTeam == nil: return nil
- case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .debug:
+ case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true)
return headerView
@@ -532,8 +571,9 @@ extension SettingsViewController
let section = Section.allCases[section]
switch section
{
+ case _ where isSectionHidden(section): return nil
case .signIn where self.activeTeam != nil: return nil
- case .signIn, .patreon, .appRefresh:
+ case .signIn, .patreon, .appRefresh, .macDirtyCow:
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(footerView, for: section, isHeader: false)
return footerView
@@ -547,9 +587,10 @@ extension SettingsViewController
let section = Section.allCases[section]
switch section
{
+ case _ where isSectionHidden(section): return 1.0
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
- case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .debug:
+ case .signIn, .account, .patreon, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height
@@ -562,9 +603,10 @@ extension SettingsViewController
let section = Section.allCases[section]
switch section
{
+ case _ where isSectionHidden(section): return 1.0
case .signIn where self.activeTeam != nil: return 1.0
- case .account where self.activeTeam == nil: return 1.0
- case .signIn, .patreon, .appRefresh:
+ case .account where self.activeTeam == nil: return 1.0
+ case .signIn, .patreon, .appRefresh, .macDirtyCow:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
@@ -801,7 +843,7 @@ extension SettingsViewController
}
- case .account, .patreon, .instructions, .techyThings: break
+ case .account, .patreon, .instructions, .techyThings, .macDirtyCow: break
}
}
}
diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift
index ecfd300d..4337319b 100644
--- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift
+++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift
@@ -52,6 +52,7 @@ public extension UserDefaults
@NSManaged var trustedSourceIDs: [String]?
@NSManaged var trustedServerURL: String?
+ @nonobjc
var activeAppsLimit: Int? {
get {
return self._activeAppsLimit?.intValue
@@ -69,6 +70,8 @@ public extension UserDefaults
}
@NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber?
+ @NSManaged var ignoreActiveAppsLimit: Bool
+
class func registerDefaults()
{
let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0)
@@ -88,7 +91,8 @@ public extension UserDefaults
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
#keyPath(UserDefaults.requiresAppGroupMigration): true,
#keyPath(UserDefaults.menuAnisetteList): "https://servers.sidestore.io/servers.json",
- #keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io"
+ #keyPath(UserDefaults.menuAnisetteURL): "https://ani.sidestore.io",
+ #keyPath(UserDefaults.ignoreActiveAppsLimit): false
] as [String : Any]
UserDefaults.standard.register(defaults: defaults)
diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift
index b27c88e6..26d3871a 100644
--- a/AltStoreCore/Model/InstalledApp.swift
+++ b/AltStoreCore/Model/InstalledApp.swift
@@ -12,8 +12,22 @@ import CoreData
import AltSign
import SemanticVersion
-// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
-public let ALTActiveAppsLimit = 3
+extension InstalledApp
+{
+ public static var freeAccountActiveAppsLimit: Int {
+ if UserDefaults.standard.ignoreActiveAppsLimit
+ {
+ // MacDirtyCow exploit allows users to remove 3-app limit, so return 10 to match App ID limit per-week.
+ // Don't return nil because that implies there is no limit, which isn't quite true due to App ID limit.
+ return 10
+ }
+ else
+ {
+ // Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
+ return 3
+ }
+ }
+}
public protocol InstalledAppProtocol: Fetchable
{
diff --git a/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift b/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift
index 880ae8fe..9c01f15f 100644
--- a/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift
+++ b/AltStoreCore/Model/Migrations/Policies/InstalledAppPolicy.swift
@@ -50,7 +50,7 @@ class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy
// We can assume there is an active app limit,
// but will confirm next time user authenticates.
- UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
+ UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
}
return NSNumber(value: isActive)