diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift
index c659971f..1a21be0f 100644
--- a/AltStore/My Apps/MyAppsViewController.swift
+++ b/AltStore/My Apps/MyAppsViewController.swift
@@ -991,7 +991,7 @@ private extension MyAppsViewController
@objc func presentInactiveAppsAlert()
{
- let message: String
+ var message: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
@@ -1000,6 +1000,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/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift
index 1fd89ff4..509ea38e 100644
--- a/AltStore/Operations/AuthenticationOperation.swift
+++ b/AltStore/Operations/AuthenticationOperation.swift
@@ -245,7 +245,7 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
{
- UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
+ UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
}
else
{
diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard
index 5415411e..90460b79 100644
--- a/AltStore/Settings/Settings.storyboard
+++ b/AltStore/Settings/Settings.storyboard
@@ -518,6 +518,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -603,6 +643,7 @@
+
diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift
index 9888a521..f1ebef70 100644
--- a/AltStore/Settings/SettingsViewController.swift
+++ b/AltStore/Settings/SettingsViewController.swift
@@ -25,6 +25,7 @@ extension SettingsViewController
case instructions
case techyThings
case credits
+ case macDirtyCow
case debug
}
@@ -70,6 +71,7 @@ class SettingsViewController: UITableViewController
@IBOutlet private var accountTypeLabel: UILabel!
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
+ @IBOutlet private var enforceThreeAppLimitSwitch: UISwitch!
@IBOutlet private var versionLabel: UILabel!
@@ -146,6 +148,7 @@ private extension SettingsViewController
}
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
+ self.enforceThreeAppLimitSwitch.isOn = !UserDefaults.standard.ignoreActiveAppsLimit
if self.isViewLoaded
{
@@ -209,6 +212,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: "")
}
@@ -225,6 +238,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
@@ -279,6 +306,16 @@ private extension SettingsViewController
UserDefaults.standard.isBackgroundRefreshEnabled = 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()
{
@@ -376,6 +413,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
@@ -404,9 +442,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
@@ -420,8 +459,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
@@ -435,9 +475,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
@@ -450,9 +491,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 .signIn, .patreon, .appRefresh, .macDirtyCow:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
@@ -520,7 +562,7 @@ extension SettingsViewController
case .refreshAttempts: break
}
- 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 2b0ac0f4..0f3c512f 100644
--- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift
+++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift
@@ -41,6 +41,7 @@ public extension UserDefaults
@NSManaged var trustedSourceIDs: [String]?
+ @nonobjc
var activeAppsLimit: Int? {
get {
return self._activeAppsLimit?.intValue
@@ -58,6 +59,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)
@@ -72,7 +75,8 @@ public extension UserDefaults
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,
#keyPath(UserDefaults.localServerSupportsRefreshing): localServerSupportsRefreshing,
- #keyPath(UserDefaults.requiresAppGroupMigration): true
+ #keyPath(UserDefaults.requiresAppGroupMigration): true,
+ #keyPath(UserDefaults.ignoreActiveAppsLimit): false,
]
UserDefaults.standard.register(defaults: defaults)
diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift
index b23926df..9511e15c 100644
--- a/AltStoreCore/Model/InstalledApp.swift
+++ b/AltStoreCore/Model/InstalledApp.swift
@@ -11,8 +11,22 @@ import CoreData
import AltSign
-// 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)